前言
在对比 C 与 C++ 的差异时,本质区别在于编程范式:C 语言采用面向过程,而 C++ 基于面向对象。在 C++ 中,类(Class)是面向对象的基础抽象单元,实例化(Instantiation)则是将抽象转化为具体可操作对象的关键。
一、类的详解
面向对象编程中的类是一种抽象数据类型,作为对象的蓝图,定义了具有相同属性和行为的一组对象的共同特征。通过类可以确定对象的基本结构和行为模式。
1.1 类的语法格式
定义一个类,就是告诉编译器这个自定义类型长什么样。主要包含两部分:
- 属性 (成员变量):描述它'是什么'(例如:名字、年龄)。
- 行为 (成员函数):描述它'能做什么'(例如:说话、跑步)。
class 类名 {
// 成员函数
void DoSomething();
// 成员变量
int variable;
}; // <--- 注意:这里必须有一个分号!
代码示例:定义一个 Student(学生)类:
#include <iostream>
#include <string>
using namespace std;
class Student {
// 成员函数(行为)
void SetInfo(string n, int a) { _name = n; _age = a; }
void Study() { cout << _name << " 正在学习,虽然他已经 " << _age << " 岁了。" << endl; }
// 成员变量(属性)
string _name;
int _age;
};
提示: 为了区分成员变量,一般习惯上会在成员变量前面或后面加
_或者以m开头。注意 C++ 中这并不是强制的,只是一种惯例。
1.2 访问限定符
在 C++ 中,访问限定符是实现封装的关键工具,它们决定了类中的成员(变量和函数)在什么地方可以被访问,什么地方被禁止访问。访问权限作用域从该访问限定符出现的位置开始,直到下一个访问限定符出现时为止;如果后面没有访问限定符,作用域就到类结束为止。
1.2.1 public (公有)
权限:public 修饰的成员在类外可以直接被访问,可以形象地理解为家门口的告示牌,谁路过都能看。通常用于成员函数(接口),供外部调用。
代码示例:
#include <iostream>
#include <string>
using namespace std;
class Student {
public:
// 成员函数(行为)
void SetInfo(string n, int a) { _name = n; _age = a; }
void Study() { cout << _name << " 正在学习,虽然他已经 " << _age << " 岁了。" << endl; }
// 成员变量(属性)
string _name;
int _age;
};
1.2.2 private (私有)
权限:private 修饰的成员在类外不能直接被访问,可以形象地理解为你的日记本,只有你自己(类内部)能看,连孩子(子类)都不能看。通常用于成员变量,防止外部随意修改数据(数据隐藏)。
代码示例:
#include <iostream>
#include <string>
using namespace std;
class Student {
public:
// 成员函数(行为)
void SetInfo(string n, int a) { _name = n; _age = a; }
void Study() { cout << _name << " 正在学习,虽然他已经 " << _age << " 岁了。" << endl; }
private: // 成员变量(属性)
string _name;
int _age;
};
注意: 在该段代码中
public修饰的范围是从public到private之间的代码段。通过private修饰后的成员变量不可在类的外部被访问。
1.2.3 protected (保护)
权限:protected 修饰的成员在类外不能直接被访问,但允许子类(派生类)进行使用,可以形象地理解为家里的传家宝,只有自己和孩子(子类)能用,外人不行。用途是当你希望数据对外界保密,但允许子类继承和使用。
提示: 在我们初学时姑且可以认为
protected和private是一样的,在以后学习了继承章节才能体现出他们的区别。
1.2.4 默认情况
在 C++ 编程中,当我们声明类成员(包括数据成员和成员函数)时,如果没有显式指定访问限定符(public、protected 或 private),编译器会自动为其分配默认的访问权限。
- 对于类 (class) 定义: 默认访问权限是
private。这意味着在类体中,所有未明确指定访问权限的成员都将被视为私有成员。class MyClass { int x; // 默认为 private void func(); // 默认为 private }; - 对于结构体 (struct) 定义: 默认访问权限是
public。这意味着在结构体中,所有未明确指定访问权限的成员都将被视为公有成员。struct MyStruct { int x; // 默认为 public void func(); // 默认为 public };
提示: 在 C++ 中,
class和struct几乎是一样的,唯一的区别就在于默认的访问权限不同。
1.3 类域
在 C++ 中,类域 (Class Scope) 指的是类定义的大括号 { } 包围起来的区域。形象理解,你可以把类域想象成一个'专属领地'或'围墙',在围墙里面定义的东西(变量、函数、类型),只属于这个类。出了这个围墙,别人就不知道它们是谁了,除非你明确指出'我要找某某家里的某某'。这很好的解决了命名冲突的问题,比如 Human 类里可以有一个 run() 函数,Car 类里也可以有一个 run() 函数,它们互不干扰,因为它们属于不同的'领地'。
1.3.1 为什么需要类域
在 C 语言中,如果两个库都定义了一个叫 Print 的函数,编译器就会报错(重定义),但在 C++ 中,类定义了一个新的作用域。
代码示例:Person 域里的 Print 和 Printer 域里的 Print
class Person {
void Print() { cout << "Hello world" << endl; }
};
class Printer {
void Print() { cout << "Hello world" << endl; }
};
1.3.2 访问类域
当我们需要在类外面访问类里面的成员时,就需要用到作用域限定符:'::'。它的含义是:'属于',例如 Person::Print 读作:'Person 类里的 Print'。
应用场景一:声明与定义分离
在实际开发中,我们通常在类里面只写声明(告诉编译器有什么),而把具体的实现(代码逻辑)写在类外面,这时就必须使用到作用域限定符:'::'。
代码示例:通过在 Person 类内声明 Eat 函数,在类域外定义函数
#include <iostream>
using namespace std;
class Person {
public:
// 在类域内只声明
void Eat();
};
// 在类域外定义(实现)
// 必须加 Person::,否则编译器以为这是个普通的全局函数
void Person::Eat() {
cout << "正在吃饭..." << endl;
}
如果忘记写
Person::会怎样? 编译器会认为你定义了一个叫Eat的全局函数,而不是Person类的成员函数,那么你就无法访问Person类里的私有成员变量。
应用场景二:类型也是类域的一部分
类域不仅包含变量和函数,还包含类型定义(如 enum, typedef, 嵌套类)。
代码示例:在 Screen 类,对内置类型 int 重定义
class Screen {
public:
// 定义了一个类型 pos,它只在 Screen 类域内有效
typedef int pos;
pos cursor; // 类内直接用,没问题
};
int main() {
pos x; // 错误!外面不认识 pos 是什么
// 正确:必须告诉编译器,我要用 Screen 里的 pos 类型
Screen::pos myCursor = 10;
return 0;
}
应用场景三:类域内的名称遮蔽
当函数的局部变量(通常是参数)和成员变量同名时,局部变量优先,这会导致成员变量被'遮蔽'了。
代码示例:当参数变量名与成员变量名相同时,可以通过使用 类名:: 成员变量 来进行区分
class Box {
public:
// 参数也叫 width
void setWidth(int width) { Box::width = width; }
private:
int width; // 成员变量
};
二、实例化详解
在 C++ 中,类 (Class) 和 实例化 (Instantiation) 是面向对象编程(OOP)的核心概念。其中类是对象进行一种抽象描述,是一个模型一样的东西,限定了类有哪些成员变量,这些成员变量只是声明,没有分配空间,用类实例化出对象时,才会分配空间。
为了直观地理解,我们可以使用一个经典的比喻:'图纸'与'房子'。
- 类 (Class):就像是建筑图纸,它规定了房子有几扇窗、什么颜色、怎么开门,但图纸本身不能住人,也不占用土地空间。
- 实例化 (Instantiation):就是根据图纸盖房子的过程,盖出来的房子叫做对象 (Object),房子是实实在在存在的,占用土地(内存)空间。

2.1 实例化对象
实例化对象的过程就是根据图纸盖房子的过程,盖出来的房子叫做对象 (Object),房子是实实在在存在的,占用土地(内存)空间。在内存上的栈区进行实例化 (静态分配),这是最常用的方式,系统会自动管理内存,出了作用域(比如函数结束),对象自动销毁。
- 语法:
类名 对象名; - 访问成员: 使用点操作符
.
代码示例:声明 Date 日期类,并实例化出对象 d1
class Date {
public:
void Init(int year, int month, int day) { _year = year; _month = month; _day = day; }
void Print() { cout << _year << "/" << _month << "/" << _day << endl; }
private:
// 这里只是声明,没有开空间
// 为了区别形参,在成员变量前加特殊标示,如 _
int _year;
int _month;
int _day;
};
void test01() {
// 实例化出对象 d1
Date d1;
// 通过 . 进行访问类中的函数
d1.Init(2025, 12, 21);
d1.Print();
}
2.2 对象的大小
计算一个类实例化出的对象的大小(即 sizeof(对象)),是 C++ 面试和底层编程中非常经典的问题。很多初学者会直觉地认为:对象大小 = 所有成员变量大小之和。
2.2.1 原则 1:只算'普通成员变量'
算进去的:
- 非静态成员变量:属于每个对象独自拥有的数据(如:int age,char c 等)。
- 虚函数指针:如果类中有虚函数(涉及到多态),对象内部会隐藏一个指针,通常占 4 字节(32 位系统)或 8 字节(64 位系统)。
不算进去的:
- 成员函数:函数代码存放在公共代码区,不占对象空间。
- 静态成员变量:存放在全局/静态数据区,属于类共有,不属于某个对象。
- 类型定义 (typedef, enum 等):只是定义,不占内存。
提示: 实例化的对象中只存储非静态的成员变量,而不进行存储成员函数,因为成员函数存储在公共代码区。
为什么要这样设计?(空间效率)
想象一下,你是一个老师,班里有 50 个学生(50 个对象)。
- 成员变量(私有数据): 每个学生都有自己的'作业本',张三写张三的,李四写李四的,每个人的内容不一样,必须人手一本。
- 成员函数(行为逻辑): 这是'教科书',书本里的知识点和公式(代码逻辑)对所有人都是一样的。
如果对象里包含函数代码:就像要求每个学生必须把整本教科书的内容抄写到自己的作业本上,如果有 50 个学生,教科书的内容就被复制了 50 份,这极其浪费纸张。现在的设计:教科书只有一本(放在讲台上,即公共代码区),所有学生共用这一本书,但每个人在作业本(对象内存)上写自己的答案。
2.2.2 原则 2:内存对齐 (重点)
在计算类大小前,首先得了解规则:
- 类的第一个成员对齐到和类中成员变量起始位置偏移量为 0 的地址处(第一个成员对齐到偏移量为 0 的位置)。
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。对齐数 = 编译器默认的一个对齐数与该成员变量大小的较小值。
- VS 中默认的值为 8。
- Linux 中 gcc 没有默认对齐数,对齐数就是成员自身的大小。
- 类的总大小为最大对齐数(类中每个成员变量都有一个对齐数,所有对齐数中最大的)的整数倍。
- 如果嵌套了类的情况,嵌套的类成员对齐到自己成员中最大对齐数的整数倍处,类的整体大小就是所有最大对齐数(含嵌套类成员的对齐数)的整数倍。
提示: 对于一个类而言,访问权限 public / private 不影响内存大小和对齐规则,成员依然按照声明顺序排列。
实战演练一:普通类
class MyClass {
private:
char a; // 1 字节
public:
int c; // int:4 字节 vs 默认对齐数:8 字节 对齐数:4 字节
// MyClass 类的最大对齐数:4
};
int main() {
MyClass c;
cout << sizeof(c) << endl;
}

①
char a;为类中的第一个成员,需要对齐到偏移量为 0 的位置。 ②int c;int:4 字节 vs 默认对齐数:8 字节 对齐数:4 字节。 ③ 类的最大对齐数:4。 如上图所示,该类的内存大小为 8 个字节,满足最大对齐数的整数倍。
实战演练二:类中嵌套类
// 发动机类 (Engine)
class Engine {
char type; // 1 字节
int power; // 4 字节
// Engine 自身大小:1 + (3 填充) + 4 = 8 字节
// Engine 的最大对齐数:4 (因为 int)
};
// 汽车类 (Car)
class Car {
char color; // 1 字节
Engine eng; // Engine 对象的对齐数为:4 vs 默认对齐数为:8 对齐数为:4 字节
char brand; // char:1 字节 vs 默认对齐数为:8 对齐数:1 字节
// car 类的最大对齐数为:4
};
int main() {
Car c;
cout << sizeof(c) << endl;
}

①
char color;为类中的第一个成员,需要对齐到偏移量为 0 的位置。 ②Engine eng;Engine 最大成员:4 字节 vs 默认对齐数:8 字节 对齐数:4 字节(占用 8 字节)。 ③char brand:char:1 字节 vs 默认对齐数:8 字节 对齐数:1 字节。 ④ Car 类的最大对齐数:4。 如上图所示,Car 类目前的内存大小为 13 个字节,不满足最大对齐数的整数倍,需要填充到整数倍,故而 Car 类的内存大小为 16 字节。
实战演练三:类型定义(如:typedef、enum、class 声明)
1. typedef / using (类型别名)
- 情况 A:起个别名不需要占用内存
class Person { // 只是起个名字,编译器记在小本本上:"Age 就是 int" typedef int Age; using ID = long; }; - 情况 B:定义了变量需要占用内存
class Person { // 只是起个名字,"Age 就是 int" typedef int Age; // 起个别名,"ID" 就相当于"long" using ID = long; // 真正的变量,这才是要占内存的 Age myAge; // 等同于 int myAge; (占 4 字节) ID myID; // 等同于 long myID; (占 4/8 字节) };
2. enum (枚举定义)
- 情况 A:声明枚举变量不占用内存
class TrafficLight { // 这只是告诉编译器:Color 这种类型只有 3 种可能的值 // 这是一个规则定义,不是数据 enum Color { RED = 0, YELLOW = 1, GREEN = 2 }; // 没有定义 Color 类型的变量! }; - 情况 B:声明了枚举类型并进行定义变量,需要占用内存
class TrafficLight { // 1. 定义类型 (不占内存) enum Color { RED, YELLOW, GREEN }; // 2. 定义变量 (占内存!) Color currentLight; };
3. 嵌套类
- 情况 A:定义了新类型不占用内存
class Outer { public: // 只是定义了一种新类型叫 Inner,就像定义 enum 或 typedef 一样 // 不占用内存 class Inner { int y; // 4 字节 int z; // 4 字节 }; }; - 情况 B:定义了变量需要占用内存
class Outer { public: // 只是定义了一种新类型叫 Inner,就像定义 enum 或 typedef 一样 // 不占用内存 class Inner { int y; // 4 字节 int z; // 4 字节 }; // 定义变量 (占内存!) Inner in; };
实战演练四:包含静态变量和成员函数
// 情况 2:包含静态成员和函数
class WithStatic {
char c; // 1
int i; // 4
static int s; // 不算!
void func() {} // 不算!
// 结果依然是 8
};

提示: 静态变量和成员函数均不占用内存,因为静态变量存储在静态区,成员函数存储在公共代码段。
2.2.3 原则 3:空类(特殊)
空类的大小是 1,这是类和结构体的一个显著区别点(在 C 语言中空结构体大小可能是 0,但在 C++ 中空类必须占位)。
class Empty { };
int main() {
cout << sizeof(Empty); // 输出 1
return 0;
}
原因: 为了保证每个实例化的对象在内存中都有独一无二的地址。
2.2.4 总结
| 内存区域 | 存放内容 | 归属权 | 这里的算进 sizeof(对象) 吗? |
|---|---|---|---|
| 栈/堆 | 非静态成员变量 (int a, char b) | 归属于对象 (每个对象各有一份) | 算!只有这些才是本体 |
| 静态/全局区 | 静态成员变量 (static int count) | 归属于类 (所有对象共享一份) | 不算! |
| 代码区 | 成员函数 (void fun()) | 归属于类 (所有对象共用一份代码) | 不算! |
三、this 指针
3.1 什么是 this 指针
this 指针本质是一个对象指针,作为 C++ 类机制中连接'对象'与'成员函数'的隐形桥梁。
this 指针形式: 类名 * const this
3.2 this 指针的引入
代码示例:声明了一个学生类
class Student {
public:
int age;
void SetAge(int a) { age = a; // 这里的 age 实际上是 this->age }
};
int main() {
Student s1;
s1.SetAge(18); // s1 调用
Student s2;
s2.SetAge(20); // s2 调用
}
我们之前已经非常清楚地认识到:'成员函数存在公共代码区,不占对象内存'。那么成员函数是如何区分是对象 s1 调用,还是对象 s2 调用的呢?答案就是:靠 this 指针偷偷传纸条。
编译器处理后的代码(实际发生的): 编译器做了一场'手术',把对象地址作为参数传了进去。
class Student {
public:
int age;
// 1. 函数定义变了:多了一个名为 this 的指针参数
// 函数相当于变成了 void SetAge(Student *const this, int a)
void SetAge(int a) { age = a; // 这里的 age 实际上是 this->age }
};
// 2. 调用方式变了:把 s1 的地址 (&s1) 传进去
int main() {
Student s1;
s1.SetAge(18); // 相当于调用了 s1.SetAge(&s1, 18);
Student s2;
s2.SetAge(20); // 相当于调用了 s2.SetAge(&s2, 20);
}
结论:
this指针保存的就是当前调用该函数的对象的地址。 ① 当你调用s1.SetAge(18),this就指向s1。 ② 当你调用s2.SetAge(20),this就指向s2。
3.3 this 指针的特性
- 只能在成员函数内部使用: 全局函数、静态成员函数(static)里没有
this指针。 - 指针被 const 修饰:
this的类型是类名* const,这意味着你不能修改this指针本身的指向(不能写this = nullptr或this = &otherObj),但你可以修改this指向的对象的内容。 - this 指针的生命周期: 它作为函数的参数(形参),在函数调用时创建,函数结束时销毁,它不占用对象的存储空间(通常通过寄存器 ecx 传递)。
- C++ 规定: 不能在实参和形参的位置显示的写
this指针 (编译时编译器会处理),但是可以在函数体内显示使用this指针。
3.4 this 指针的使用
场景一:解决命名冲突
当函数的形参和成员变量同名时,形参会遮挡成员变量。
class Student {
public:
void SetAge(int age) {
this->age = age; // 把形参 age 赋给当前对象的成员 age
}
private:
int age;
};
场景二:链式调用
如果你想让代码写成 obj.func1().func2().func3() 这种'流水线'风格,函数需要返回对象本身。
class Calculator {
public:
Calculator() { sum = 0; }
// 返回引用,意味着返回对象本体,而不是拷贝
Calculator& add(int n) {
sum += n;
return *this; // 返回当前对象的引用
}
private:
int sum;
};
int main() {
Calculator calc;
// 链式调用:先加 1,结果 (对象本身) 再加 2,再加 3
calc.add(1).add(2).add(3);
}
3.5 一个经典的'坑':空指针调用
class A {
public:
void Print() { cout << "Hello" << endl; }
void PrintAge() { cout << age << endl; }
private:
int age;
};
int main() {
A* p = nullptr; // p 是空指针
p->Print(); // 第一行:会不会崩?
p->PrintAge(); // 第二行:会不会崩?
}
详情解析: ①
p->Print():不会崩!编译器将其转化为Print(p)。虽然传进去的this指针是nullptr,但this(没有访问任何成员变量),它只是打印一个字符串。所以能正常运行。 ②p->PrintAge():会崩!编译器转化为PrintAge(p)。函数内部试图访问age,这等价于this->age。因为this是nullptr,访问nullptr->age会导致段错误。
为了让你彻底理解'为什么',我们需要拆解成三个层面来讲:
1. 物理层面:代码复用 (为了省内存)
我们在之前的对话中提到过:成员函数不占对象内存,它存在公共代码区。这意味着,无论你实例化了 100 个对象还是 1000 个对象,Print 函数的代码在内存里只有独一份。既然代码只有一份,它的内存地址就是固定的(假设地址是 0x400800)。当你调用 p->Print() 时,CPU 实际上是执行了一条指令:call 0x400800(跳转到该函数地址去执行),而不是出现解引用的操作。
问题来了:既然大家都跳转到同一个地方去执行代码,函数怎么知道现在是处理 A 对象的数据,还是 B 对象的数据呢?解决方案:必须把对象的地址当成参数带过去!这就是 Print(p) 的由来,p 就是那个必须要带过去的'参数'。
2. 为什么 nullptr 调用不崩?(核心逻辑)
当我们写:
class A {
public:
void Print() { cout << "Hello" << endl; }
private:
int age;
};
int main() {
A* p = nullptr; // p 是空指针
p->Print();
}
编译器将转化为:
class A {
public:
void Print() { cout << "Hello" << endl; }
private:
int age;
};
int main() {
A* p = nullptr; // p 是空指针
Print(p);
}
此时发生了什么?
- ① 传参: 把
p的值(也就是 0 或 NULL)作为第一个参数传给函数,这一步是合法的,传个 0 进去怎么了?不会崩。 - ② 跳转: CPU 跳转到
Print函数的代码地址开始执行,这一步也是合法的,代码就在那,跑过去就行。 - ③ 函数内部执行:
- 如果函数里写的是
cout << "Hello":它不需要读取寄存器里的那个 0,直接打印字符串,程序正常运行。 - 如果函数里写的是
cout << this->age:CPU 尝试去读取 0 地址偏移量的内存(比如 0x0000 + 4),访问 0 地址是非法的 -> 操作系统拦截 -> 崩溃。
- 如果函数里写的是
相信看了上述解释,你也能够征服面试官!

