初识C++、其中的引用、类(class)和结构体(struct)
C++ 入门:从编程思想到 Hello World
1. 面向过程 vs 面向对象
在学习 C++ 之前,先要理解它和 C 语言最大的不同:
C 是 面向过程 的语言,而 C++ 是 面向对象 的语言。
1.1 面向过程(Procedure-Oriented Programming, POP)
面向过程的核心思想是:
以函数为中心,先写出数据,然后用函数一步步操作数据。
示例(C 风格):
struct Player {int x, y;int speed;
};void Move(struct Player* p, int xa, int ya) {p->x += xa * p->speed;p->y += ya * p->speed;
}
特点:
Player
保存数据。Move
是单独的函数,专门处理Player
。数据和函数是分离的。
优点是 简单、直接,但代码规模大了以后容易难以维护。
1.2 面向对象(Object-Oriented Programming, OOP)
面向对象的核心思想是:
以对象为中心,把数据和操作数据的函数绑定在一起。
示例(C++ 风格):
class Player {
public:int x, y;int speed;void Move(int xa, int ya) {x += xa * speed;y += ya * speed;}
};
特点:
数据(成员变量)和方法(成员函数)在一个类里。
使用时只需要通过对象调用:
Player player; player.Move(1, -1);
这样更清晰、可扩展。
1.3 为什么叫“面向对象”
在 OOP 里:
类(Class)是模具。
对象(Object)是产品。
class Dog {
public:void Bark() { std::cout << "Woof!\n"; }
};
Dog d1, d2; // 两个对象
每个对象都能叫,但属性可能不同(颜色、体重)。
1.4 OOP 三大特征
封装(Encapsulation) → 数据和方法放在对象里。
比如:x, y
定义在Player
里,移动操作必须通过Move()
方法完成。继承(Inheritance) → 子类继承父类,复用功能。
比如:class Dog : public Animal { ... }
。多态(Polymorphism) → 同样的方法,不同对象实现不同效果。
比如:Cat.Speak()
输出“Meow”,Dog.Speak()
输出“Woof”,但我们都可以调用Animal.Speak()
。
1.5 总结一句话
面向过程:以函数为中心,数据和函数分离。
面向对象:以对象为中心,数据和函数合为一体。
C++ 中的 main
函数与 cout
输出
学习 C++ 时,最经典的入门例子就是 Hello World 程序。虽然代码很短,但里面其实隐藏了不少知识点。今天我们来拆解一下:
#include <iostream>int main() {std ::cout << "Hello World" << std ::endl;return 0;
}
1. main
函数的返回值
在 C++ 中,main
函数是程序的入口。它和其他函数有一个小小的区别:
对于一般函数:
必须明确指定返回值类型,比如int
、float
、void
等。对于
main
函数:
虽然标准写法是int main()
,但如果你没有写return 0;
,编译器会默认帮你返回0
,表示程序正常结束。
也就是说:
只有 main
函数可以“偷懒”不写返回值,其他函数要么写明返回值类型,要么声明为 void
。
2. std::cout
与流式输出
来看这一句:
std::cout << "Hello World" << std::endl;
这里的 <<
其实不是普通的“左移运算符”,而是 C++ 对运算符的重载。
可以把它理解为一个函数调用,它的作用是:
把右边的内容(
"Hello World"
) 推送进左边的输出流(std::cout
)。std::cout
是 C++ 标准输出流对象,对应 终端屏幕。std::endl
表示 换行并刷新缓冲区,确保内容立刻显示。
因此,这一行代码就等价于:
把字符串
"Hello World"
输出到屏幕,并换行。
3. std::
的作用
很多初学者会疑惑:为什么前面要加 std::
?
这是因为 cout
、cin
、string
等标准库里的东西,全都定义在 std
命名空间(namespace)里面。
std::cout
就是告诉编译器:我用的是std
命名空间里的cout
。如果你不写
std::
,要么编译器报错,要么你得写一句:
using namespace std;
这样之后你就能直接写
cout << "..." << endl;
而不用每次加 std::
前缀。
不过在 小练习代码里用 using namespace std;
没问题,但是在 工程代码里推荐保留 std::
。
因为 using namespace std;
会把整个标准库的名字都引入,容易和你自己写的变量或函数名冲突。
C++ 中的引用(Reference)
在 C++ 里,引用(Reference
)常常被称为 指针的语法糖。它能做的事情,其实都可以用指针来完成,但语法更加直观,使用起来更安全。因此在日常开发中,我们更推荐使用引用而不是指针。
什么是引用?
引用并不是一个新的变量,而是一个已经存在变量的 别名。这意味着引用本身并不会单独占用内存空间。
int a = 5;
int& ref = a; // ref 是 a 的别名
ref = 2; // 等价于 a = 2
std::cout << a << std::endl; // 输出 2
上面的例子中,int& ref = a;
这一句声明了一个引用。此时对 ref
的任何操作,其实就是对 a
的操作。
注意:这里的 &
并不是取地址运算符,而是 类型声明的一部分,表明这是一个引用。
为什么要使用引用?
考虑这样一个例子:
// 整型变量递增函数(无效版本)
void Increment(int x) {x++;
}int a = 5;
Increment(a);
std::cout << a << std::endl; // 输出还是 5
这里 a
并没有像我们期望的那样加 1。原因是函数参数 x
是 值传递,即 a
的值被复制了一份传给 x
,对 x
的修改不会影响 a
。
方法 1:指针方式
我们可以用指针传递变量的地址来修改原始值:
void Increment(int* x) {(*x)++; // 注意括号优先级
}int a = 5;
Increment(&a);
std::cout << a << std::endl; // 输出 6
这里我们把 a
的地址传给了函数,函数内部通过解引用 *x
修改了 a
。
方法 2:引用方式
C++ 引入了引用,使得代码更直观:
void Increment(int& x) {x++;
}int a = 5;
Increment(a);
std::cout << a << std::endl; // 输出 6
这里 x
就是 a
的别名,直接操作 x
就等价于操作 a
,不用再写解引用符号 *
。
引用的限制
引用一旦绑定到某个变量,就 不能再改为引用别的变量。
看这个例子:
int a = 5;
int b = 8;int& ref = a; // ref 是 a 的别名
ref = b; // 注意:这不是让 ref 指向 b,而是把 b 的值赋给 a
结果:
a = 8
b = 8
很多初学者会以为 ref
改成引用 b
了,但其实并不是这样。引用在定义的时候就必须绑定某个变量,之后不能再改变。
如果想更改“引用指向谁”怎么办?
那就必须回到指针:
int a = 5;
int b = 8;int* ref = &a; // ref 指向 a
ref = &b; // 现在 ref 改成指向 b
这里 ref
不是引用,而是指针,可以随意改变它指向的对象。
C++ 中的类(class)与结构体(struct)
在 C++ 中,类(class)和结构体(struct)本质上没有引入“新的能力”,它们只是对已有功能的一种更好的组织方式。类是一种 语法糖,它的目的就是把 数据和操作这些数据的函数 放在一起,以便让代码更有结构,更容易维护。
定义一个类
来看一个例子:
class Player
{int x, y;int speed;
};
这里我们定义了一个新的类型 Player
,以后就可以像定义 int
一样来定义它的对象:
Player player; // 创建一个 Player 类型的变量(对象)
player
就是 Player
的一个 对象(object),也叫做 实例(instance)。这一步操作就是所谓的 实例化(instantiation)。
成员的可见性(访问权限)
如果你尝试写:
player.x = 5;
会报错:Player::x 不可访问
。这是因为在 C++ 中,类的成员默认是 private 的,只能在类的内部访问,外部无法直接访问。
我们需要明确指定可见性:
class Player
{
public:int x, y;int speed;
};
加上 public:
后,player.x
、player.y
就可以在类外访问了。
总结:
class 的成员默认是
private
。struct 的成员默认是
public
。
给类添加方法
我们希望 Player
能移动。最简单的做法是写一个函数:
void Move(Player& player, int xa, int ya)
{player.x += xa * player.speed;player.y += ya * player.speed;
}
这样调用:
Move(player, 1, -1);
但更好的方式是直接把这个函数放进类里,这时候它就叫做 方法(method):
class Player
{
public:int x, y;int speed;void Move(int xa, int ya){x += xa * speed;y += ya * speed;}
};
调用方法:
player.Move(1, 0);
这里不需要传入 player
,因为方法本身就在对象里,x
、y
、speed
就代表这个对象的成员。
class 和 struct 的区别
唯一的区别在于 默认可见性:
class
的成员默认是private
struct
的成员默认是public
其它方面完全一样。
因此:
如果你希望所有成员都是
public
,又懒得写public:
,可以用struct
。如果你要写一个完整的类层次(包含继承、多态等),就用
class
。
这就是为什么在 C++ 中我们经常说:
POD(Plain Old Data) 类型(单纯的数据组合,不包含复杂逻辑),用
struct
。复杂逻辑的对象,用
class
。
示例:向量类(Vec2)
struct Vec2
{float x, y;void Add(const Vec2& other){x += other.x;y += other.y;}
};
Vec2
就是一个典型的 POD 类型,主要用来存放数据,同时加上一些简单的辅助方法。
示例:日志类(Log)
再看一个功能性更强的例子:
#include <iostream>class Log
{
public:const int LogLevelError = 0;const int LogLevelWarning = 1;const int LogLevelInfo = 2;private:int m_LogLevel = LogLevelInfo; // 默认打印所有日志public:void SetLevel(int level){m_LogLevel = level;}void Error(const char* message){if (m_LogLevel >= LogLevelError)std::cout << "[ERROR]: " << message << std::endl;}void Warn(const char* message){if (m_LogLevel >= LogLevelWarning)std::cout << "[WARNING]: " << message << std::endl;}void Info(const char* message){if (m_LogLevel >= LogLevelInfo)std::cout << "[INFO]: " << message << std::endl;}
};int main()
{Log log;log.SetLevel(log.LogLevelWarning);log.Warn("Hello World");log.Error("Hello World");log.Info("Hello World");
}
输出结果:
[WARNING]: Hello World
[ERROR]: Hello World
因为我们设置了 LogLevelWarning
,所以只打印 Warning 及以上等级的日志。
命名约定
注意到类中有个成员 m_LogLevel
,带有 m_
前缀。这是一种常见的编码约定,用来表示 这是一个类的成员变量,便于区分局部变量和成员变量。