文章目录
-
- [1. C++在C后的延伸](#1. C++在C后的延伸)
- [2. 类的基本骨架:成员、类域和定义位置](#2. 类的基本骨架:成员、类域和定义位置)
-
- [2. 类定义中放什么](#2. 类定义中放什么)
- [2.2 类内定义与类外定义](#2.2 类内定义与类外定义)
- [2.3 class 和 struct 的差别](#2.3 class 和 struct 的差别)
- [3. 访问限定符:封装不是仅仅把数据藏起来](#3. 访问限定符:封装不是仅仅把数据藏起来)
-
- [3.1 public 和 private](#3.1 public 和 private)
- [3.2 protected 先知晓边界即可](#3.2 protected 先知晓边界即可)
- [4. 对象大小:成员变量属于对象,成员函数不按对象重复保存](#4. 对象大小:成员变量属于对象,成员函数不按对象重复保存)
- [5. this 指针:成员函数如何找到当前对象](#5. this 指针:成员函数如何找到当前对象)
-
- [5.1 从两个对象调用同一个函数](#5.1 从两个对象调用同一个函数)
- [5.2 `this` 的几个性质](#5.2
this的几个性质) - [5.3 空指针调用成员函数为什么有时不马上崩溃](#5.3 空指针调用成员函数为什么有时不马上崩溃)
- [7. 小结](#7. 小结)
1. C++在C后的延伸
C++在保留了大量C的表达方式的同时,增加了更强的类型组织和抽象工具,一些数据与操作数据的函数可以被组织到同一个类中,这正是C++面向对象的开始。
在C中,我们可能这样表示一个日期:
cpp
typedef struct Date
{
int year;
int month;
int day;
} Date;
void DateInit(Date* d, int year, int month, int day)
{
d->year = year;
d->month = month;
d->day = day;
}
void DatePrint(const Date* d)
{
printf("%d/%d/%d\n", d->year, d->month, d->day);
}
调用时必须把对象地址显式传给每一个函数:
cpp
Date d;
DateInit(&d, 2026, 9, 24);
DatePrint(&d);
这种写法可以工作,但是它把"哪些函数属于Date"交给了命名习惯来维护。函数写错参数、直接修改不应该暴露的数据与忘记检查日期等等操作是否合乎规则,都只能依靠调用者自觉,安全性较低。
而C++类则把这层关系写进类型本身:
cpp
class Date
{
public:
void Init(int year, int month, int day);
void Print() const;
private:
int _year = 0;
int _month = 0;
int _day = 0;
};
调用时变成:
cpp
Date d;
d.Init(2026, 9, 24);
d.Print();
2. 类的基本骨架:成员、类域和定义位置
2. 类定义中放什么
cpp
class Counter
{
public:
void Add(int value)
{
_value += value;
}
int Get() const
{
return _value;
}
private:
int _value = 0;
};
这里定义的是一个计数器对象,其拥有其的私有成员变量_value,然后拥有两个成员函数,一个用于获取当前计数,一个用于增加指定数量。
class Counter { ... }; 中的内容叫类体。类体里的变量叫成员变量,函数叫成员函数。类定义最后的分号不能省略,因为整个类定义在语法上也是一条声明。
下面我们创建两个对象first和second,让其增加不同的计数:
cpp
Counter first;
Counter second;
first.Add(3);
second.Add(10);
std::cout << "first当前计数:" << first.Get() << std::endl;
std::cout << "second当前计数:" << second.Get() << std::endl;
运行示例:

可以看到,first和second是两个对象,各自拥有一份_value。
2.2 类内定义与类外定义
代码量较少的成员函数可以直接写在类内;较长的函数则通常在类外定义,让类体更像一份接口说明:
cpp
class Counter
{
public:
void Add(int value);
int Get() const;
private:
int _value = 0;
};
void Counter::Add(int value)
{
_value += value;
}
int Counter::Get() const
{
return _value;
}
Counter::Add 中的::表示"到 Counter 这个类域中寻找Add"。如果省略 Counter::,编译器会把它当成普通的全局函数,无法直接访问 _value。
对于成员函数的函数头后加的
const表示的是此函数不会修改对象的相关成员变量,是保护数据的一种方式,如果函数不需要修改成员变量的话建议加上,增加代码可靠性。

2.3 class 和 struct 的差别
C++ 中的 struct 也可以包含成员函数和访问限定符:
cpp
struct Point
{
void Print() const;
int _x = 0;
int _y = 0;
};
但两者的主要默认规则不同:class 的默认访问权限是 private,struct 的默认访问权限是 public。
C++ 中可以继续使用
struct表示简单数据,也可以使用class表示需要封装行为的类型。关键不在于关键字哪个"更高级",而在于你是否清楚哪些成员应该暴露给外部
struct:适合纯数据载体(类似 C 结构体),数据默认公开,简单方便;class:适合需要封装的类型,内部数据默认私有,控制对外接口。
3. 访问限定符:封装不是仅仅把数据藏起来
3.1 public 和 private
cpp
class Counter
{
public:
void Add(int value)
{
if (value > 0)
{
_value += value;
}
}
int Get() const
{
return _value;
}
private:
int _value = 0;
};
当外部想要使用成员变量_value时,只能通过 Add 和 Get 使用:
cpp
SafeCounter counter;
counter.Add(5);
counter._value = -100; // 编译错误:_value 是 private
封装的价值是把规则集中在类内部。以后如果计数器需要限制最大值,只改
Add就可以;调用者不需要知道_value的具体表示方式。
访问限定符的作用范围从它出现的位置开始,一直持续到下一个访问限定符或类定义结束(默认是private):
cpp
class Example
{
int _privateValue = 0; // 默认 private
public:
void Set(int value)
{
_privateValue = value;
}
private:
int _anotherValue = 0;
};
3.2 protected 先知晓边界即可
protected 在类外不能直接访问,和 private 在当前阶段的表现相同。它主要会在继承中体现差异。开始学习类和对象时,只需要重点明白:需要对外提供的操作放到 public,表示细节的数据和辅助函数通常放到 private。
4. 对象大小:成员变量属于对象,成员函数不按对象重复保存
观察下面的类型:
cpp
class Small
{
public:
void Print() const {}
private:
int _value = 0;
};
class Empty
{
public:
void Print() const {}
};
第一个Small类中有一个成员变量,而Empty中无成员变量,让我们看看其各自的对象大小:
cpp
std::cout << "Small类大小:" << sizeof(Small) << std::endl;
std::cout << "Empty类大小:" << sizeof(Empty) << std::endl;
运行示例:

可以看到,Small 至少需要容纳一个 int(4个字节)。Empty 没有成员变量,通常仍然占 1 个字节,用来区分不同对象的地址。
成员函数编译后是指令,不需要在每个对象里复制一份,因此一般不计入对象大小。
这就引出了成员变量与成员函数的不同存放方式:
成员变量 vs 成员函数
- 成员变量: 每个对象独立一份,其对应内部的成员变量有各自的内存空间。
- 成员函数: 整个程序里只有一份,所有对象共用。
成员函数的代码通常保存一份,调用时会根据对象不同而操作对应的成员变量。
其内部会依靠编译器隐式传入的
this指针实现。
类的大小实际和C中结构体相似,还是会受到内存对齐影响,规则类似:
- 第一个成员通常从对象起始地址开始;
- 后续成员可能为了满足对齐要求产生填充;
sizeof是编译器根据布局规则计算出的结果,不要只用成员大小简单相加。
5. this 指针:成员函数如何找到当前对象
5.1 从两个对象调用同一个函数
cpp
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print() const
{
std::cout << _year << '/' << _month << '/' << _day << std::endl;
}
private:
int _year = 0;
int _month = 0;
int _day = 0;
};
int main()
{
Date d1;
Date d2;
d1.Init(2026, 9, 24);
d2.Init(2027, 1, 1);
d1.Print();
d2.Print();
}
Init 的函数体没有写 d1 或 d2,但两次调用结果不同。原因就是是非静态成员函数会有一个编译器隐含传入的参数:this。
可以把调用过程近似理解为:
text
d1.Init(2026, 9, 24) -> 类似 Init(&d1, 2026, 9, 24)
d2.Init(2027, 1, 1); -> 类似 Init(&d2, 2027, 1, 1)
在真实的 C++ 语法中,我们不需要在调用时手写这个参数,但成员函数内部确实可以使用 this:
cpp
void Date::Init(int year, int month, int day)
{
this->_year = year;
this->_month = month;
this->_day = day;
}
在编写时通常可以省略 this->,因为编译器知道 _year 是当前对象的成员。显式写出this指针常用于成员变量和参数同名时,或者是为了强调当前对象。
5.2 this 的几个性质
- this 是成员函数内部可用的指针;
- 它指向调用当前成员函数的对象;
- 不能修改 this 让它指向另一个对象;
- 静态成员函数没有 this,因为它不依附于某个具体对象。
5.3 空指针调用成员函数为什么有时不马上崩溃
观察并运行以下代码:
cpp
class Demo
{
public:
void Hello() const
{
std::cout << "hello" << std::endl;
}
void PrintValue() const
{
std::cout << _value << std::endl;
}
private:
int _value = 10;
};
int main() {
Demo* ptr = nullptr;
ptr->Hello(); // 可能看起来能运行
ptr->PrintValue(); // 访问成员数据,通常会崩溃
}
运行示例:


从语言规则上说,通过空指针调用非静态成员函数本身就是未定义行为,不能因为某次运行没有崩溃就认为它是安全的。
PrintValue 需要通过this 找到 _value,所以更容易触发崩溃。
注意不要把这种使用方式当作技巧,而是通过这个例子帮助我们理解
this指针的存在与作用。
7. 小结
类不是把 C 语法换一种写法,而是把数据、操作和访问边界组织到一个类型中。对象拥有自己的状态,成员函数通过隐含的 this 找到当前对象,访问限定符则决定外部能否直接触碰这些状态。理解这三层关系后,构造函数、析构函数和运算符重载就不再是孤立语法,而是继续完善"对象类型"的工具。