在从 C 语言迈向 C++ 的过程中,面向对象编程(OOP)是最核心的转变。在 C 语言中,我们习惯于用结构体(struct)来组织单纯的数据;但在 C++ 的世界里,"类(Class)"赋予了数据以生命。
今天,我将系统总结 C++ 中关于类的声明、权限控制、函数定义,并深度剖析构造函数与"初始化成员列表"的必用场景。
一、 类(Class)与 结构体(Struct)的核心区别
在 C++ 中,声明一个类与声明一个结构体非常相似(声明本身只是一个模板,不占用实际内存)。
⚠️ 经典误区纠正: 很多人以为"结构体只能存数据,类才能写函数"。其实在现代 C++ 中,结构体(struct)和类(class)都可以包含变量和函数 。它们之间唯一的本质区别在于默认的访问权限:
-
struct:默认访问权限是public(公开的)。 -
class:默认访问权限是private(私有的)。
💻 代码验证
#include <iostream>
using namespace std;
// C++ 中的 Struct,默认是 public
struct MyStruct {
int age = 10;
void print() { cout << "Struct 可以有函数!" << endl; }
};
// C++ 中的 Class,默认是 private
class MyClass {
int age = 20; // 默认是私有,外部不可见
void print() { cout << "Class 也是私有!" << endl; }
};
int main() {
MyStruct s;
s.print(); // ✅ 正常运行,外部可随意访问
MyClass c;
// c.print(); // ❌ 编译报错!因为 class 默认是私有的,外部无法调用
return 0;
}
二、 三大访问修饰符
为了保证数据的安全性,C++ 提供了三种访问修饰符,用于精确控制数据和函数的访问级别:
-
public(公有):声明的函数和变量,既可以在类内部访问,也可以在外部直接访问。 -
private(私有):只能在类的内部(类的成员函数中)访问,外部绝对不可见。这完美体现了面向对象的"封装"特性。 -
protected(受保护) :在外部不可访问,但允许在派生类(子类)中被访问。主要用于继承机制。
💻 代码示例:
class BankAccount {
private:
int password; // 核心密码:只能内部修改,外部绝对看不到
protected:
int balance; // 账户余额:外部不能碰,但以后的"子类卡"可以碰
public:
// 公开接口:提供给外部操作的合法途径
void setPassword(int p) {
password = p; // 内部函数可以自由访问私有变量
}
};
int main() {
BankAccount myCard;
myCard.setPassword(123456); // ✅ 正常,调用公开函数
// myCard.password = 123456; // ❌ 报错!私有变量不能直接修改
return 0;
}
三、 成员函数的类内与类外定义
类内部声明的函数有两种实现方式,根据代码长短和工程习惯,我们可以灵活选择:
-
类内定义:直接在类的花括号内写完函数体。适合简短的函数。
-
类外定义 :类内部只写声明,外部写实现。此时函数名前必须加上作用域解析符
类名::。适合复杂的函数,让类的结构一目了然。
💻 代码示例:
#include <iostream>
using namespace std;
class Dog {
public:
// 1. 类内定义
void eat() {
cout << "吃骨头" << endl;
}
// 2. 类外定义的第一步:内部只写声明
void bark();
};
// 2. 类外定义的第二步:外部写实现,必须加 Dog::
void Dog::bark() {
cout << "汪汪汪!" << endl;
}
int main() {
Dog dog1;
dog1.eat();
dog1.bark();
return 0;
}
四、 构造函数(Constructor)与函数重载
当类被创建(实例化)时,系统会自动执行一个特殊的函数------构造函数。它的主要任务是:在对象分配内存后,立刻为类中的变量进行初始化。
构造函数的铁律:
-
名字与类名完全相同。
-
没有返回值(连
void都不能写)。 -
默认生成:如果你不写,编译器会自动生成一个隐藏的"无参构造函数"。
-
函数重载:构造函数支持重载。你可以写多个同名构造函数,通过传入不同个数或类型的参数,编译器会自动匹配。
💻 代码示例:
#include <iostream>
#include <string>
using namespace std;
class Student {
public:
string name;
// 构造函数重载 1:无参
Student() {
name = "未知";
cout << "调用了无参构造函数" << endl;
}
// 构造函数重载 2:有参
Student(string n) {
name = n;
cout << "调用了有参构造函数,姓名:" << name << endl;
}
};
int main() {
Student s1; // 自动调用无参构造函数
Student s2("Tom"); // 自动匹配并调用有参构造函数
return 0;
}
五、 必须使用"初始化成员列表"的 3 种极限场景
在普通的构造函数内部,我们可以通过 = 为普通变量赋值。但在以下三种特殊情况下,必须使用"初始化成员列表"进行赋值:
-
常量(
const修饰的变量) -
引用(
&修饰的别名)原因:常量和引用的底层特性决定了,它们必须在内存分配的那一瞬间就完成初始化,绝不允许"先分配内存,事后再赋值"。如果写在构造函数体内用
=赋值,编译器会直接报错。 -
嵌套类对象(且该类没有无参构造函数)
原因:如果在类 A 中嵌套了类 B 的对象,而类 B 只定义了带参数的构造函数(这会导致其默认的无参构造函数失效)。当创建类 A 时,编译器虽然知道该分配多少内存,但不知道该传什么参数去初始化类 B。此时必须在 A 的初始化列表中显式调用 B 的构造函数。
格式: 类名(形参) : 成员1(值), 成员2(值) {}
💻 终极代码示例:
#include <iostream>
using namespace std;
// 被嵌套的类 B
class Engine {
public:
int power;
// 注意:这里定义了带参构造,默认的无参构造就失效了!
Engine(int p) { power = p; }
};
// 类 A
class Car {
private:
const int wheels; // 极限场景 1:常量
int& ref_id; // 极限场景 2:引用
Engine myEngine; // 极限场景 3:嵌套了没有无参构造函数的类
public:
// 必须使用冒号引出的"初始化成员列表"!
Car(int w, int& id, int p) : wheels(w), ref_id(id), myEngine(p)
{
// 这里是普通的构造函数体
cout << "汽车制造完毕!" << endl;
cout << "轮子数量:" << wheels << endl;
cout << "马力:" << myEngine.power << endl;
cout << "出厂编号:" << ref_id << endl;
}
};
int main() {
int id = 888;
// 创建类时,传入对应参数
Car myCar(4, id, 500);
return 0;
}
六、 析构函数(Destructor):对象的"临终遗言"
如果说构造函数是对象诞生时的"出生证明",那么析构函数就是对象生命周期结束时的"临终遗言"。当一个对象的作用域结束被销毁时,系统会自动调用析构函数。
析构函数的 4 条铁律:
-
命名规则 :名字必须是 波浪号
~加上类名 (例如~Student())。 -
无形参、无返回 :它没有任何参数,连
void都不能写。 -
绝对不能重载 :因为没有参数,所以一个类只能有一个析构函数。
-
主要使命 :清理战场。如果你的对象在活着的时候向系统借了资源(比如用
new在堆区申请了内存、打开了某个文件等),必须 在析构函数里把它们还回去(用delete释放内存或关闭文件),否则就会造成内存泄漏。
提示: 和构造函数一样,如果你不写析构函数,编译器也会在后台默默生成一个什么都不做的"默认析构函数"。但只要你用了指针和动态内存,就必须手动写析构函数。
💻 终极代码示例(构造与析构的完美配合):
#include <iostream>
#include <cstring>
using namespace std;
class Computer {
private:
char* cpu_name; // 一个指针,准备指向动态分配的内存
public:
// 1. 构造函数:对象创建时自动运行
Computer(const char* name) {
// 使用 C++ 的 new 关键字,在堆区申请一块内存
cpu_name = new char[strlen(name) + 1];
strcpy(cpu_name, name);
cout << "【诞生】构造函数被调用,电脑组装完毕!CPU: " << cpu_name << endl;
}
// 2. 析构函数:对象销毁时自动运行
~Computer() {
// 对象死前,必须用 delete[] 把 new 出来的内存还给系统
delete[] cpu_name;
cout << "【死亡】析构函数被调用,电脑报废,内存已成功回收!" << endl;
}
};
void testFunction() {
cout << "--- 进入测试函数 ---" << endl;
Computer myPc("Intel Core i9"); // 此时对象被创建,调用构造函数
cout << "--- 准备离开测试函数 ---" << endl;
} // 函数执行到这个右大括号时,myPc 对象的寿命结束,自动调用析构函数!
int main() {
testFunction();
cout << "程序即将彻底结束。" << endl;
return 0;
}