详细介绍:C++入门基础:类与对象一篇通关
如果你总觉得 C++ 类和对象“懂了一半、写不顺手”,那这篇就是专门写给你的。我们不从概念堆砌开始,而是从你最熟悉的 C 结构体入手,一步步长成一个真正能在项目里跑的 C++ 类:先搞清楚对象在内存里怎么存,再拆开构造 / 析构 / 拷贝 / 赋值到底分别管什么;然后再把运算符重载、static、友元、lambda、返回值优化这些零碎概念,统统塞进一个清晰的框架里。整篇文章的目标只有一个——看完之后,你不但敢自己设计类,而且能看懂、改动别人写的类,用起来心里有数。
目录
一、从 C 到 C++:为什么需要“类”?
1.1 用 C 写栈:数据和操作是分开的
先看纯 C 写一个栈(Stack)长什么样:
typedef int STDataType;
typedef struct Stack
{
STDataType* a; //动态数组指针
int top; //栈顶下标
int capacity; //当前容量
} Stack;
void STInit(Stack* ps);
void STDestroy(Stack* ps);
void STPush(Stack* ps, STDataType x);
void STPop(Stack* ps);
STDataType STTop(Stack* ps);
int STEmpty(Stack* ps);
int STSize(Stack* ps);
使用时:
int main()
{
Stack st;
STInit(&st);
STPush(&st, 1);
STPush(&st, 2);
printf("%d\n", STTop(&st));
STDestroy(&st);
return 0;
}
特点:
Stack这个结构体只放“数据成员”;操作栈的行为(入栈、出栈、销毁……)都是分散在外部的一堆函数;
每次都要把
Stack*传来传去,还得自己记得调STDestroy释放内存。
1.2 用 C++ 写栈:数据 + 行为“打包”
C++ 把“数据 + 操作”放在一起,用 class 来描述:
#include
using namespace std;
typedef int STDataType;
class Stack
{
public:
void Init(int n = 4)
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (_a == nullptr)
{
perror("malloc 失败");
return;
}
_capacity = n;
_top = 0;
}
void Push(STDataType x)
{
//先不管扩容细节
_a[_top++] = x;
}
STDataType Top()
{
assert(_top > 0);
return _a[_top - 1];
}
void Destroy()
{
free(_a);
_a = nullptr;
_capacity = _top = 0;
}
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
int main()
{
Stack st;
st.Init();
st.Push(1);
st.Push(2);
cout << st.Top() << endl;
st.Destroy();
return 0;
}
对使用者来说:
它看到的是一个“整体”:
Stack st; st.Push(...);内部到底怎么存,动态数组也好,链表也罢,对外都是“黑盒”。
这就是**封装(encapsulation)**的核心思想:
把数据和操作数据的方法捆在一起;
对外只提供必要的接口,内部细节不暴露。
1.3 类的基本结构
类的“语法壳子”长这样:
class 类名
{
public:
//公有成员:外部可以访问
//可以是函数、变量、类型别名等
protected:
//受保护成员:外部访问不了,子类可以访问
private:
//私有成员:只有本类成员函数能访问
}; //这里必须有分号!!!
注意最后这个分号,超多人第一次写类会漏掉,然后收获一个报错。
成员函数可以:
直接在类中“声明 + 定义”;
也可以在类里只“声明”,然后在类外用
类名::函数名定义,后面会专门讲。
二、class vs struct、访问限定符与类作用域
2.1 class 和 struct
对 C 来说:
struct只是“打包数据”的壳;class是 C++ 加出来的,代表“面向对象的一整个类型”。
但在 C++ 里,两者几乎是双胞胎:
class A
{
public:
void func() {}
private:
int x;
};
struct B
{
void func() {}
int x;
};
区别主要就两点:
默认访问权限不同:
class默认private(私有)struct默认public(公有)
使用习惯:
复杂类型 / 封装良好的,一般用
class简单“数据包”(比如配置、简单 POD)喜欢用
struct
本质上:你用 struct 也能写完整继承、多态,只是读代码的人会有点心理不适。
2.2 访问限定符
可以理解成“三道门锁”:
public:谁都能来;protected:子类能进,外部进不来;private:只有“自己家”能用。
例子:
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;
};
在 main 里:
int main()
{
Date d;
d.Init(2024, 7, 5);
d.Print();
//d._year = 9999; //报错:_year 是 private
return 0;
}
2.3 类域与作用域运算符 ::
类本身也是一个“作用域”,类似命名空间。
你可以在类里写声明:
class Stack
{
public:
void Init(int n = 4); //这里只是声明
void Destroy();
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
然后在类外实现:
void Stack::Init(int n)
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (_a == nullptr)
{
perror("malloc 失败");
return;
}
_capacity = n;
_top = 0;
}
void Stack::Destroy()
{
free(_a);
_a = nullptr;
_capacity = _top = 0;
}
Stack::Init 可以读作:
“Stack 这个类里的 Init 函数”。
三、对象、内存与 this 指针
3.1 对象是怎么创建出来的?
类只是“图纸”,对象才是真正占内存的“房子”。
常见几种创建方式:
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1; //栈上的对象(自动对象)
Date d2; //再来一个
Date arr[10]; //对象数组(栈上)
Date* p1 = new Date; //堆上的对象
Date* p2 = new Date[5]; //堆上对象数组
p1->Init(2024, 7, 5); //对堆对象,用 -> 调成员
delete p1; //对应 new
delete[] p2; //对应 new[]
return 0;
}
3.2 对象的大小
常见误解:
“类里有好多成员函数,那对象应该很大吧?”
实际上,成员函数不占每个对象的空间。
对象里只放“成员变量”,成员函数的代码只有一份,放在代码区,被所有对象共享。
例子:
class A
{
};
class B
{
public:
void Func1() {}
void Func2() {}
private:
int x;
};
class C
{
public:
static int scount;
};
int main()
{
cout << sizeof(A) << endl; //通常是 1
cout << sizeof(B) << endl; //通常是 4(假设 int 4 字节)
cout << sizeof(C) << endl; //通常也是 1,static 不算在对象里
return 0;
}
解释:
空类对象不会是 0,一般编译器会塞 1 字节“占位”,防止两个空对象在内存上地址一样;
static成员变量放在静态区,全类共享一份,不计入单个对象的sizeof。



3.3 this 指针
例子:
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;
};
main中:
Date d1, d2;
d1.Init(2024, 3, 31);
d2.Init(2024, 7, 5);
Init 只有一份代码,问题来了:它怎么知道自己现在应该改 d1,还是改 d2?
编译器会把它悄悄翻译成这样:
void Date::Init(Date* const this, int year, int month, int day)
{
this->_year = year;
this->_month = month;
this->_day = day;
}
原版本:
d1.Init(2024, 3, 31); //隐式翻译成 Init(&d1, 2024, 3, 31);
d2.Init(2024, 7, 5); //Init(&d2, 2024, 7, 5);
所以:
this是一个隐含参数,类型大致是Date* const;在成员函数中访问
_year,其实是this->_year的省略写法;你也可以显式写
this->_year,更直观。
3.4 空指针
看一眼这段代码:
class A
{
public:
void Print1()
{
cout << "Print1()" << endl;
}
void Print2()
{
cout << _a << endl;
}
private:
int _a = 10;
};
int main()
{
A* p = nullptr;
p->Print1(); // ?
p->Print2(); // ?
return 0;
}
本质上:
p->Print1()→Print1(p),里面没访问成员,只是打印一句话,所以不会解引用 p,不会炸;p->Print2()→Print2(p),里面访问了this->_a,相当于nullptr->_a,直接越界崩溃。
结论:
语法上允许“空指针 调用 成员函数”;
安不安全,看函数内部有没有用到成员变量 / 解引用 this。
3.5 const 成员函数中的 this
普通成员函数里,this 类型可以理解为:
Date* const this;
加上 const:
void Print() const
{
//this 变成:const Date* const this;
}
也就是:
在
const成员函数中,编译器会强制你“不能改成员变量”(除非用mutable标记);这样一来,
const Date对象就可以放心调用这些函数。
这就是“const 对象只能调 const 成员函数”的底层逻辑。
四、构造函数
类的基础概念:
一个普通类,编译器默认会帮你准备一套“基础成员函数”:
默认构造函数(无参或全缺省构造)
析构函数
拷贝构造函数
赋值运算符
operator=取地址运算符
operator&(及 const 版本)

后面我们讲构造 / 析构 / 拷贝 / 赋值,其实就在把这几个“默认”讲透。
4.1 构造函数的语法和使用方式
例子:
class Date
{
public:
//全缺省构造:既可以无参,也可以带参
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
特点:
函数名和类名完全一样;
没有返回值类型(连
void都不能写);会在对象“出生”那一刻自动执行;
可以重载多个构造;
如果你一个构造都不写,编译器会给你生成一个“啥都不做”的默认构造。
使用:
int main()
{
Date d1; // 走默认参数:1-1-1
Date d2(2024, 7, 5); // 指定年月日
Date d3(2024); // 2024-1-1
Date d4 = Date(2024, 8, 8); // 创建匿名对象再拷贝构造,通常会被优化成直接构造
d1.Print();
d2.Print();
d3.Print();
d4.Print();
return 0;
}
4.2 错误写法“Date d3();”
Date d3(); // 很像在构造对象,但其实不是!
这一行在编译器眼里是:
声明了一个函数:
名字叫 d3,返回类型是 Date,参数列表为空。
正确写法:
Date d3; // 真正定义一个 Date 对象
Date d4{}; // C++11 风格,等价于默认构造
4.3 组合类中的构造
例子:
class Time
{
public:
Time(int hour = 0)
: _hour(hour)
{}
private:
int _hour;
};
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
, _t(12) // 成员对象 _t
{}
private:
int _year;
int _month;
int _day;
Time _t;
};
构造顺序是:
先构造成员对象:
_year、_month、_day、_t,顺序就是它们在类中“声明”的顺序;再执行
Date构造函数体里的代码(大括号里的部分)。
不管你在初始化列表里写成什么顺序,真正构造顺序只看“成员声明顺序”。
4.4 匿名对象
Date(2024, 7, 5).Print();
这里 Date(2024, 7, 5) 是一个匿名对象:
编译器会在这一行“临时造一个 Date”;
调完
Print(),这行语句结束,匿名对象立刻调用析构函数。
典型用途:
写一些“临时用一下就丢”的对象;
RAII 风格的资源管理工具:构造时加锁,析构时解锁。
五、析构函数
5.1 析构函数的写法
以 Stack 为例:
class Stack
{
public:
Stack(int n = 4)
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (_a == nullptr)
{
perror("malloc 失败");
return;
}
_capacity = n;
_top = 0;
}
~Stack()
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_capacity = _top = 0;
}
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
析构函数特点:
名字是
~类名;没有参数,也没有返回值;
也可以自己写,也可以让编译器默认生成(什么都不做);
触发时机:
栈上对象:出作用域就会自动析构;
静态 / 全局对象:程序结束时析构;
堆对象:
delete或delete[]时析构。
5.2 什么时候“必须”手动写析构?
像 Date 这种类:
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
: _year(year), _month(month), _day(day)
{}
private:
int _year;
int _month;
int _day;
};
成员全是内置类型(int 等),编译器默认析构“啥都不做”完全够用;
你写不写析构几乎没差。
但 Stack 这种类:
里面有
malloc/new出来的堆空间;你不释放,就会泄漏;
所以必须认真写析构函数,把资源释放干净。
“谁申请,谁释放;谁管理资源,谁定义析构”。
5.3 组合类的析构顺序
class MyQueue
{
public:
// 就算你不写析构,编译器也会生成一个默认析构,
// 里面会自动调用 _pushst 和 _popst 的析构。
private:
Stack _pushst;
Stack _popst;
};
析构时:
先“反方向”析构成员对象:先
_popst,再_pushst(先构造后析构,后构造先析构);再析构
MyQueue自己。
你只要把 Stack 析构写对,MyQueue 的默认析构就自动帮你做完工作了。
六、拷贝构造
6.1 拷贝构造什么时候会被调用?
以 Date 为例:
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
: _year(year), _month(month), _day(day)
{}
Date(const Date& d) // 拷贝构造
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
三种典型触发场景:
1)用一个对象初始化另一个对象
Date d1(2024, 7, 5);
Date d2(d1); // 调拷贝构造
Date d3 = d1; // 也是拷贝构造,不是赋值
2)按值传参
void Func(Date d) // 形参是“值传递”,要拷贝一份进来
{
// ...
}
int main()
{
Date d1(2024, 7, 5);
Func(d1); // 触发拷贝构造
}
3)按值返回
Date MakeDate()
{
Date tmp(2024, 7, 5);
return tmp; // 理论上会拷贝构造一个返回值副本
}
不过这里编译器经常会做优化。
6.2 默认拷贝构造(浅拷贝)
如果你不写拷贝构造,编译器会给你一个“默认版本”:
对于类里的每个成员变量,都按顺序“拷一份”;
内置类型:按值复制;
指针:只复制指针本身(地址),不会新开堆空间。
对 Date 这种类没问题;
对带指针的类,就出事故了。

6.3 浅拷贝的问题
class Stack
{
public:
Stack(int n = 4)
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (_a == nullptr)
{
perror("malloc fail");
return;
}
_capacity = n;
_top = 0;
}
~Stack()
{
free(_a);
_a = nullptr;
_capacity = _top = 0;
}
// 没写拷贝构造,编译器自动生成“浅拷贝”版本
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
int main()
{
Stack st1;
st1.Push(1);
st1.Push(2);
Stack st2 = st1; // 浅拷贝:st2._a 和 st1._a 指向同一块堆内存
return 0; // 退出 main 时:
// 1)先析构 st2,free(_a)
// 2)再析构 st1,又 free(_a)
// 同一块堆内存被 free 两次,直接炸
}
所以,一旦类里有“自己管理的资源”,默认拷贝构造是危险的,要自己写深拷贝版本。
6.4 深拷贝拷贝构造
class Stack
{
public:
Stack(int n = 4)
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (_a == nullptr)
{
perror("malloc fail");
return;
}
_capacity = n;
_top = 0;
}
// 深拷贝拷贝构造
Stack(const Stack& st)
{
_a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);
if (_a == nullptr)
{
perror("malloc fail");
return;
}
memcpy(_a, st._a, sizeof(STDataType) * st._top);
_capacity = st._capacity;
_top = st._top;
}
~Stack()
{
free(_a);
_a = nullptr;
_capacity = _top = 0;
}
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
深拷贝的要点:
给自己重新申请一块堆空间;
把对方的元素“拷贝一份”进来;
两个对象的
_a完全独立,谁 free 谁负责。
6.5 组合类中的拷贝:MyQueue 的例子
class MyQueue
{
public:
// 不写拷贝构造也可以,编译器会默认生成:
// 依次调用 _pushst 和 _popst 的拷贝构造
private:
Stack _pushst;
Stack _popst;
};
int main()
{
MyQueue q1;
MyQueue q2 = q1; // 会触发 Stack 的拷贝构造
return 0;
}
只要 Stack 的拷贝构造是“深拷贝”,MyQueue 的默认拷贝构造就也是安全的。
七、赋值运算符重载
7.1 运算符重载?
如果不写 operator=:
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
: _year(year), _month(month), _day(day)
{}
private:
int _year;
int _month;
int _day;
};
下面这句:
Date d1(2024, 7, 5);
Date d2(2024, 8, 5);
d2 = d1; // 默认赋值:对每个成员做简单赋值
编译器会自动帮你写一个“成员逐个赋值”的版本。
对纯值类型(int、double 等)没问题;
对 Stack 这种带堆指针的类,又会踏入“浅拷贝 + 双重释放”的坑。
7.2 手写赋值运算符
用 Date 模板:
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
: _year(year), _month(month), _day(day)
{}
Date& operator=(const Date& d)
{
if (this != &d) // 1. 防止自赋值
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this; // 2. 支持 d1 = d2 = d3 这种链式赋值
}
private:
int _year;
int _month;
int _day;
};
对 Stack 来说:
先判断是不是自赋值:
if (this == &st) return *this;释放自己当前持有的堆空间;
重新
malloc一块新的;把对方的数据拷贝过来;
return *this;
7.3 为什么返回 *this 的引用?
为了能写出:
d1 = d2 = d3;
执行顺序是:
先执行
d2 = d3,返回d2的引用;再执行
d1 = (这个引用)。
如果你返回的是“值”,会多一次拷贝;
如果你不返回 引用,那就没办法链式赋值。
八、运算符重载:让类用起来更自然
8.1 运算符重载的基本规则
你不能造新运算符,只能重载已有的符号;
运算符重载本质上还是“函数”,只是长得更像数学运算;
有些运算符不能重载(比如
.、?:、sizeof等)。
重载的目的不是“炫技”,而是:
让你写出来的类,更像个“内置类型”,用起来自然、直观。
8.2 Date 的比较运算符
给 Date 实现 < 和 ==:
class Date
{
public:
// ...
bool operator<(const Date& d) const
{
if (_year < d._year) return true;
if (_year > d._year) return false;
if (_month < d._month) return true;
if (_month > d._month) return false;
return _day < d._day;
}
bool operator==(const Date& d) const
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
private:
int _year;
int _month;
int _day;
};
其他比较运算符可以基于这两个推出来:
bool operator<=(const Date& d1, const Date& d2)
{
return d1 < d2 || d1 == d2;
}
bool operator>(const Date& d1, const Date& d2)
{
return d2 < d1;
}
bool operator>=(const Date& d1, const Date& d2)
{
return !(d1 < d2);
}
bool operator!=(const Date& d1, const Date& d2)
{
return !(d1 == d2);
}
8.3 自增 / 自减
假设我们让 Date 支持“加一天”:
Date d(2024, 7, 5);
++d; // 前置 ++:先 +1 再使用
d++; // 后置 ++:先使用,再 +1
对应写法:
// 前置 ++
Date& Date::operator++()
{
// 给当前日期 +1 天
// ...
return *this;
}
// 后置 ++
Date Date::operator++(int) // 参数 int 只是个“占位”
{
Date tmp(*this); // 保存旧值
// 当前日期 +1 天
// ...
return tmp; // 按旧值返回
}
int 这个参数没用到,就是为了让编译器区分“前置版本”和“后置版本”。
8.4 日期 + 天数、日期差
常见需求:
d2 = d1 + 300;—— 从 d1 往后推 300 天;d1 += 10;—— 在原基础上加 10 天;n = d2 - d1;—— 两个日期相差多少天。
这几个运算符的具体实现逻辑大概是:
持续累加天数,每超过当前月天数,就往下一个月滚;
到 12 月再往后,就进下一年;
求差时,可以把小日期往大日期一点点走,一边走一边计数(暴力写法),也可以换算成“天数序号”之后做减法(更高效)。
原讲义里的 Date 类会给出一个完整实现,建议你对照着自己敲一遍,感受一下日期运算里各种边界(闰年、大小月)的处理。
8.5 输入输出运算符
想做到:
Date d1(2024, 4, 14);
cout << d1 << endl;
Date d2;
cin >> d2; // 比如输入:2024 7 5
一般写成“友元 + 全局函数”的组合:
class Date
{
// 声明成友元,方便内部访问私有成员
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
public:
Date(int year = 1, int month = 1, int day = 1)
: _year(year), _month(month), _day(day)
{}
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "-" << d._month << "-" << d._day;
return out;
}
istream& operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
这样就可以像输出普通类型一样输出 / 输入自定义的 Date。
九、const 对象与 const 成员函数
9.1 const 对象能做什么、不能做什么?
const Date d1(2024, 7, 5);
这个对象的含义是:
你不能改它的内容(成员变量);
也不能调用有可能修改它的成员函数。
所以我们要给不修改对象的函数,加上 const 标签:
class Date
{
public:
void Print() const // 告诉编译器:我不改这个对象
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
使用:
int main()
{
Date d2(2024, 8, 5);
d2.Print(); // 非 const 对象,调 const 成员函数没问题
const Date d1(2024, 7, 5);
d1.Print(); // const 对象,也能调(因为 Print 是 const)
return 0;
}
总结:
const 对象 只能调 const 成员函数;
非 const 对象:啥都能调,const 的也能调。
9.2 const 成员函数内的 this
总结:
普通成员函数里,
this是Date* const;加了
const的成员函数里,this是const Date* const。
意思是:
你不能通过
this改成员变量;写了也会直接编译报错。
十、初始化列表
10.1 初始化列表语法
例子:
class Time
{
public:
Time(int hour)
: _hour(hour) // 初始化列表
{
cout << "Time()" << endl;
}
private:
int _hour;
};
配合 Date:
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
初始化列表可以理解成:
“在构造函数体执行之前,先用括号里的这些值,把成员对象挨个初始化。”
10.2 哪些成员必须用初始化列表?
以下类型的成员,你不用初始化列表,就根本没法初始化:
1.引用成员
class A
{
public:
A(int& x)
: _ref(x) // 必须在初始化列表里绑定引用
{}
private:
int& _ref;
};
2. const 成员
class B
{
public:
B(int x)
: _n(x) // const 成员必须在初始化列表中给初值
{}
private:
const int _n;
};
3.没有默认构造函数的成员对象
class Time
{
public:
Time(int hour);
// 没有无参构造
};
class C
{
public:
C()
: _t(12) // 必须在初始化列表里指定参数
{}
private:
Time _t;
};
10.3 初始化顺序
只看“声明顺序”,不看你写的顺序
例如:
class Demo
{
public:
Demo(int x)
: _b(x) // 写在前面
, _a(_b) // 写在后面
{}
private:
int _a;
int _b;
};
真正执行顺序是:
先初始化
_a(因为它在类里先声明);再初始化
_b。
所以,_a(_b) 使用 _b 的时候,_b 其实还没被初始化,逻辑错误。
总结:
初始化列表里写的顺序只是“好看”;
真正的初始化顺序 = 成员声明的顺序;
推荐你初始化列表的顺序也按声明顺序来写,避免误导自己。
很多考试题喜欢问这个,比如:
class A
{
public:
A(int a)
: _a2(a)
, _a1(_a2)
{}
void Print()
{
cout << _a1 << " " << _a2 << endl;
}
private:
int _a1;
int _a2;
};
问输出多少?
关键就是你要意识到:先初始化 _a1,再初始化 _a2。
10.4 声明处默认值 + 初始化列表
C++11 之后,你可以在成员声明处直接给默认值:
class Date
{
public:
Date()
: _month(2) // 这里覆盖了下面的默认值
{
cout << "Date()" << endl;
}
private:
int _year = 1; // 声明处默认值
int _month = 1;
int _day = 1;
};
规则:
如果初始化列表里写了某个成员的初始化,就用初始化列表里的;
否则,如果声明处有默认值,就用声明处默认值;
都没有的话:
对内置类型成员:值不确定(栈上局部对象时就是“随机垃圾值”);
对类类型成员:会自动调用它的默认构造函数。
十一、类型转换
11.1 内置类型 → 类类型
这就是单参数构造的“隐式转换”。
class A
{
public:
// 如果不加 explicit,这就是一个可以隐式转换的构造函数
// explicit A(int a1)
A(int a1)
: _a1(a1)
{}
void Print() const
{
cout << _a1 << " " << _a2 << endl;
}
private:
int _a1 = 1;
int _a2 = 2;
};
使用:
int main()
{
A aa1 = 1; // 1 被自动转换成 A(1)
aa1.Print();
return 0;
}
如果你不想要这种“自动转换”(怕出 bug),可以给构造函数加个 explicit:
explicit A(int a1);
这样 A aa1 = 1; 就会直接编译错误,必须写成 A aa1(1); 才行。
11.2 类类型 → 类类型
这是一种 转换构造。
class A
{
public:
A(int a1, int a2)
: _a1(a1), _a2(a2)
{}
int Get() const
{
return _a1 + _a2;
}
private:
int _a1;
int _a2;
};
class B
{
public:
B(const A& a) // A → B 的“转换构造”
: _b(a.Get())
{}
private:
int _b;
};
使用:
int main()
{
A a(1, 2);
B b = a; // 编译器会自动调用 B(const A&) 做转换
return 0;
}
十二、static 成员
12.1 static 成员变量
class A
{
public:
A()
{
++_scount;
}
A(const A& t)
{
++_scount;
}
~A()
{
--_scount;
}
static int GetACount()
{
return _scount;
}
private:
static int _scount; // 只声明,不定义
};
// 在类外定义并初始化这一个“全局变量”
int A::_scount = 0;
使用:
int main()
{
cout << A::GetACount() << endl; // 0
A a1;
cout << A::GetACount() << endl; // 1
A a2(a1);
cout << A::GetACount() << endl; // 2
return 0;
}
特点:
_scount是所有 A 对象共享的一份数据;存在静态区,而不是每个对象里;
必须在类外定义一次:
int A::_scount = 0;
12.2 static 成员函数
class A
{
public:
static int GetACount()
{
// 这里没有 this,不能访问非 static 成员
return _scount;
}
private:
static int _scount;
};
使用方式:
int n = A::GetACount();
// 也可以 a1.GetACount(); 但语义上没区别
特点:
没有
this指针;只能访问静态成员(静态变量 / 静态函数);
经常用来写“和类紧密相关,但不依赖某个具体对象”的工具函数。
12.3 例题
题目大意:求1+2+3+...+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。
可以用一个类 + 静态成员:
class Sum
{
public:
Sum()
{
_ret += _i;
++_i;
}
static int GetRet()
{
return _ret;
}
private:
static int _i;
static int _ret;
};
int Sum::_i = 1;
int Sum::_ret = 0;
class Solution {
public:
int Sum_Solution(int n) {
Sum arr[n]; // 构造 n 个对象
return Sum::GetRet(); // 构造时负责累加
}
};
构造函数被调用 n 次,每次把 _i 加进 _ret,再 _i++。
最后 _ret = 1+2+…+n。
这就是“利用构造函数 + static 静态变量”构造出来的小技巧。
十三、友元
13.1 友元函数
class B; // 提前声明
class A
{
friend void func(const A& aa, const B& bb); // 把 func 声明成友元
private:
int _a1 = 1;
int _a2 = 2;
};
class B
{
friend void func(const A& aa, const B& bb);
private:
int _b1 = 3;
int _b2 = 4;
};
void func(const A& aa, const B& bb)
{
cout << aa._a1 << endl; // 直接访问 private
cout << bb._b1 << endl;
}
友元函数的理解:
“这个函数不是我的成员,但我信它,
可以让它直接访问我的 private / protected 成员。”
13.2 友元类
class A
{
friend class B; // B 是 A 的友元类
private:
int _a1 = 1;
int _a2 = 2;
};
class B
{
public:
void func1(const A& aa)
{
cout << aa._a1 << endl; // 可以访问 A 的私有成员
cout << _b1 << endl;
}
void func2(const A& aa)
{
cout << aa._a2 << endl;
cout << _b2 << endl;
}
private:
int _b1 = 3;
int _b2 = 4;
};
友元类的权限 = 这个类里所有成员函数都是友元函数。
13.3 友元的利与弊
优点:
在某些场景(自定义输出、迭代器访问容器内部结构)会非常方便;
能解决“我想保持封装,又希望某个外部函数/类能看到内部细节”的矛盾。
缺点:
破坏封装边界,用多了会让类之间高度耦合;
维护起来成本高:一个 friend 改动,可能影响一大片。
经验:
能不用尽量不用,用了要心里有数。
十四、内部类
14.1 内部类的定义与使用
C++ 允许你在一个类的里面,再定义另一个类:
class Outer
{
public:
class Inner // 内部类
{
public:
void Print()
{
cout << "Inner::Print" << endl;
}
};
void Test()
{
Inner in;
in.Print();
}
};
使用方式:
int main()
{
Outer::Inner in; // 使用“外部类::内部类”访问
in.Print();
return 0;
}
特点:
内部类本质上就是一个“普通类”,只是作用域在
Outer里面;通常用于:把某个“仅服务于这个类的工具类型”藏在内部。
典型例子:STL 容器的迭代器:
vector<int>::iteratorlist<int>::iterator
逻辑上只属于某个容器类型,用内部类表达很自然。
14.2 内部类访问外部类成员
默认情况下,内部类没有外部类的 this 指针,也就不能直接访问外部类成员。
想访问就:
把外部类对象的引用/指针作为参数传进来;
再通过它访问对应成员。
这和“普通类之间互相访问”没有本质区别。
十五、取地址运算符重载
正常情况下:
Date d;
Date* p = &d; // & 返回的是 d 在内存中的真实地址
你也可以重载 operator&:
class Date
{
public:
Date* operator&()
{
// 可以 return this;
return nullptr; // 甚至可以故意隐藏真实地址(强行封装)
}
const Date* operator&() const
{
return this;
}
private:
int _year;
int _month;
int _day;
};
不建议乱用:
一般人看到
&d,直觉认为这是对象真地址;如果你重载成返回别的东西,会把后来的维护者整疯掉。
这玩意儿只在一些特殊的调试 / 框架级黑魔法里出现过,对日常写项目、刷题来说:
知道“可以重载,但几乎没人用”就够了。
十六、匿名函数(lambda)
上面我们讲了“匿名对象”,现在讲一个很常用的“匿名函数”:lambda 表达式。
16.1 lambda基本格式
标准格式:
[捕获列表](参数列表) -> 返回类型
{
函数体
};
例子:
auto add = [](int a, int b) -> int {
return a + b;
};
int main()
{
cout << add(1, 2) << endl; // 输出 3
return 0;
}
如果返回类型可以从 return 推导出来,可以省略 -> int:
auto add = [](int a, int b) {
return a + b; // 编译器自动推断为 int
};
16.2 lambda 怎么看到外部变量?
“捕获列表”用来说明:我要把哪些外部变量带进这个匿名函数里用。
几种常见写法:
int x = 10;
int y = 20;
auto f1 = [x]() { cout << x << endl; }; // 值捕获 x
auto f2 = [&x]() { x++; }; // 引用捕获 x
auto f3 = [=]() { cout << x << " " << y; }; // 按值捕获所有用到的外部变量
auto f4 = [&]() { x++; y++; }; // 按引用捕获所有用到的外部变量
auto f5 = [=, &y]() { /* x 值捕获,y 引用捕获 */ };
简单理解:
[=]:按值捕获所有需要用的外部变量;[&]:按引用捕获所有需要用的外部变量;[x, &y]:x 拷贝一份,y 用引用。
16.3 和算法配合
最典型的用法:和 sort / for_each 之类的算法搭着用。
#include
#include
#include
using namespace std;
int main()
{
vector v = {3, 1, 5, 4, 2};
// 按绝对值从小到大排序(虽然这里都是正数,只是举例)
sort(v.begin(), v.end(),
[](int a, int b) {
return abs(a) < abs(b);
});
for_each(v.begin(), v.end(),
[](int x) { cout << x << " "; });
cout << endl;
return 0;
}
你可以把 lambda 理解为:
“我临时写了一个小函数,只在这一行里用一用,
没必要单独起名字,写到外面去。”
在“类与对象”这一块,lambda 更多是配合容器、算法、回调来用的,算是你后面写 STL / 现代 C++ 必备的武器之一。
十七、对象拷贝的编译器优化
按值返回对象时,理论上要调用拷贝构造,但实际很多时候不会。
17.1 最基本的场景:返回局部对象
Date MakeDate()
{
Date tmp(2024, 7, 5);
return tmp; // 理论上需要拷贝构造一个“返回值对象”
}
朴素想法:
在栈上构造局部变量
tmp;调用拷贝构造,把
tmp拷贝一份到“返回值对象”里;函数结束,
tmp析构;调用者用接收到的那份“返回值”。
但是这样多了一次拷贝,对性能没必要,所以编译器一般会引入一种优化:返回值优化(RVO:Return Value Optimization)。
17.2 构造优化
例子:
Date MakeDate()
{
return Date(2024, 7, 5); // 直接返回一个临时匿名对象
}
编译器会直接在调用者那里的空间“就地构造”这个 Date;
不会真的在函数内部搞一个临时对象再拷贝出去;
理论上要调用拷贝构造,实际上直接省略(叫 拷贝省略)。
NRVO(Named Return Value Optimization)则是对“有名字的局部变量”做类似优化:
Date MakeDate()
{
Date tmp(2024, 7, 5);
return tmp; // 这里 tmp 也可以被就地构造在返回值空间中
}
大部分现代编译器(例如 GCC/Clang/MSVC)都会尽量做这种优化,尤其是在 C++17 之后,某些情况下 RVO 是强制的——标准允许编译器“假装这次拷贝从没发生过”,甚至可以不调用拷贝构造函数。
17.3 和类的关系?
当你写拷贝构造 / 析构比较重的类时(比如内部有大块堆空间),合理利用 RVO 会非常关键:
写返回局部对象的工厂函数时,不用太纠结“是不是多了一次拷贝”,编译器一般会帮你优化掉;
但你也不能完全指望它——拷贝构造必须写对,即使在“没优化”的场景下,也得能跑。
比如:
Date GetMaxDate(Date a, Date b)
{
return (a < b) ? b : a;
}
这里参数传递已经各拷贝了一次,再返回时,如果没有优化,就又要拷贝一次。
但现代编译器会尽量把这些对象“就地构造”,减少不必要的拷贝。
结论:
不要因为“编译器有优化”就偷懒不写正确的拷贝构造 / 赋值运算符;
写对了以后,编译器会在此基础上进一步帮你“省掉多余的拷贝”。
完

浙公网安备 33010602011771号