
前言
大家好,我是你们的老朋友小小风呀(¯▽¯)/~~!今天我们继续【从零开始学 C++】专题的第四篇,深入学习类和对象的进阶知识。上一篇我们已经了解了类和对象的基础概念,这一篇我们要学习一些更重要的知识点:初始化列表、类型转换、static 成员、友元、内部类、匿名对象、编译器优化。
这些内容虽然看起来有点多,但只要跟着我的节奏,用大白话理解,配合代码示例,相信大家一定能轻松掌握!
目录
[1. 基础概念解释](#1. 基础概念解释)
[2. 代码示例](#2. 代码示例)
[【示例 1】必须使用初始化列表的场景](#【示例 1】必须使用初始化列表的场景)
[【示例 2】初始化顺序的坑!](#【示例 2】初始化顺序的坑!)
[1. 基础概念解释](#1. 基础概念解释)
[2. 代码示例](#2. 代码示例)
[【示例 1】隐式类型转换演示](#【示例 1】隐式类型转换演示)
[【示例 2】explicit 禁止隐式转换](#【示例 2】explicit 禁止隐式转换)
[(三)static 成员](#(三)static 成员)
[1. 基础概念解释](#1. 基础概念解释)
[2. 代码示例](#2. 代码示例)
[【示例 1】统计创建了多少个对象](#【示例 1】统计创建了多少个对象)
[【示例 2】静态成员函数没有 this 指针](#【示例 2】静态成员函数没有 this 指针)
[1. 基础概念解释](#1. 基础概念解释)
[2. 代码示例](#2. 代码示例)
[【示例 1】友元函数](#【示例 1】友元函数)
[【示例 2】友元类](#【示例 2】友元类)
[1. 基础概念解释](#1. 基础概念解释)
[2. 代码示例](#2. 代码示例)
[【示例 1】内部类基本用法](#【示例 1】内部类基本用法)
[1. 基础概念解释](#1. 基础概念解释)
[2. 代码示例](#2. 代码示例)
[【示例 1】匿名对象的生命周期](#【示例 1】匿名对象的生命周期)
[1. 基础概念解释](#1. 基础概念解释)
[2. 代码示例](#2. 代码示例)
[【示例 1】编译器优化完整演示](#【示例 1】编译器优化完整演示)
(一)再探构造函数:初始化列表
1. 基础概念解释
大白话时间:
之前我们写构造函数时,都是在函数体里面给成员变量赋值,就像这样:
cpp
Date(int year, int month, int day) {
_year = year; // 这是赋值,不是初始化!
_month = month;
_day = day;
}
但其实,这叫赋值 ,不叫初始化!真正的初始化是在对象创建的时候就完成的,就像你出生时就有了名字,而不是出生后再取名字。
初始化列表 就是真正做初始化的地方,格式是在构造函数后面加个冒号:,然后用逗号分隔每个成员的初始化:
cpp
Date(int year, int month, int day)
: _year(year) // 这才是真正的初始化!
, _month(month)
, _day(day)
{}
重要规则:
-
✅ 必须用初始化列表的三种情况:引用成员、const 成员、没有默认构造的自定义类型成员
-
✅ 成员变量的初始化顺序是按声明顺序,不是按初始化列表的顺序!
-
✅ 即使你不写初始化列表,编译器也会自动生成一个
-
✅ C++11 支持在声明时给缺省值,这个缺省值就是给初始化列表用的
2. 代码示例
【示例 1】必须使用初始化列表的场景
这个例子展示了哪些情况必须用初始化列表,否则编译报错!
cpp
#include <iostream>
using namespace std;
// Time类没有默认构造函数
class Time {
public:
Time(int hour)
: _hour(hour)
{
cout << "Time(int hour) 被调用" << endl;
}
private:
int _hour;
};
class Date {
public:
// 引用、const成员、无默认构造的自定义类型,必须在初始化列表初始化!
Date(int& x, int year = 1, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
, _t(12) // Time没有默认构造,必须在这里初始化
, _ref(x) // 引用必须初始化
, _n(100) // const成员必须初始化
{
// 如果不在上面初始化,这里就会报错!
// error: 没有合适的默认构造函数
// error: 必须初始化引用
// error: 必须初始化常量限定类型的对象
}
void Print() const {
cout << _year << "-" << _month << "-" << _day << endl;
cout << "引用值: " << _ref << ", const值: " << _n << endl;
}
private:
int _year;
int _month;
int _day;
Time _t; // 没有默认构造的类类型
int& _ref; // 引用成员
const int _n; // const成员
};
int main() {
int i = 666;//i的值是传给了int& x中的x
Date d1(i);
d1.Print();
return 0;
}
运行结果:
cpp
Time(int hour) 被调用
1-1-1
引用值: 666, const值: 100
【示例 2】初始化顺序的坑!
这个例子告诉你:初始化顺序是按声明顺序,不是按初始化列表顺序!
cpp
#include <iostream>
using namespace std;
class A {
public:
A(int a)
: _a1(a) // 第二步:初始化_a1,值为1
, _a2(_a1) // 第一步:初始化_a2,此时_a1还是随机值!
{}
void Print() {
cout << "_a1 = " << _a1 << ", _a2 = " << _a2 << endl;
}
private:
// 注意:声明顺序是 _a2 先,_a1 后!
int _a2 = 2; // 先初始化_a2
int _a1 = 2; // 后初始化_a1
};
int main() {
A aa(1);
aa.Print(); // 猜猜输出什么?答案:_a1 = 1, _a2 = 2(因为_a2用了缺省值)
return 0;
}
划重点: 一定要让初始化列表的顺序和成员声明顺序保持一致,避免踩坑!
(二)类型转换
1. 基础概念解释
大白话时间:
C++ 很 "聪明",有时候会自动帮你做类型转换。比如你写A aa = 1;,编译器会自动把1转换成 A 类型的对象,这就叫隐式类型转换。
原理是:编译器先用1构造一个临时的 A 对象,然后用这个临时对象拷贝构造aa。现代编译器会优化成直接构造。
但有时候这种自动转换会带来问题,所以 C++ 提供了explicit关键字,加上 explicit 就禁止隐式转换!
2. 代码示例
【示例 1】隐式类型转换演示
cpp
#include <iostream>
using namespace std;
class A {
public:
// 不加explicit,支持隐式类型转换
A(int a1)
: _a1(a1)
{
cout << "A(int a1) 构造" << endl;
}
// C++11支持多参数隐式转换
A(int a1, int a2)
: _a1(a1)
, _a2(a2)
{}
void Print() {
cout << "_a1 = " << _a1 << ", _a2 = " << _a2 << endl;
}
int Get() const {
return _a1 + _a2;
}
private:
int _a1 = 1;
int _a2 = 2;
};
class B {
public:
B(const A& a)
: _b(a.Get())
{
cout << "B(const A& a) 构造" << endl;
}
private:
int _b = 0;
};
int main() {
cout << "=== 单参数隐式转换 ===" << endl;
A aa1 = 1; // 1 隐式转换成A对象
aa1.Print();
cout << "\n=== 引用绑定临时对象 ===" << endl;
const A& aa2 = 1; // 临时对象具有常性,要用const引用
cout << "\n=== C++11多参数隐式转换 ===" << endl;
A aa3 = {2, 3};
aa3.Print();
cout << "\n=== 类类型之间的转换 ===" << endl;
B b = aa3; // A对象隐式转换成B对象
return 0;
}
【示例 2】explicit 禁止隐式转换
cpp
#include <iostream>
using namespace std;
class A {
public:
// 加上explicit,禁止隐式类型转换!
explicit A(int a1)
: _a1(a1)
{}
void Print() {
cout << "_a1 = " << _a1 << endl;
}
private:
int _a1;
};
int main() {
// A aa1 = 1; // 编译报错!不允许隐式转换
A aa1(1); // ✅ 显式构造可以
aa1.Print();
A aa2 = A(2); // ✅ 显式构造后拷贝也可以
aa2.Print();
return 0;
}
建议: 构造函数尽量加上explicit,避免意外的隐式转换带来 bug!
(三)static 成员
1. 基础概念解释
大白话时间:
普通成员变量是每个对象自己一份,就像每个人都有自己的钱包。但static 静态成员是整个类共享一份,就像班级的公共基金,所有同学共用这一份钱。
关键特性:
-
静态成员变量不属于某个对象,存在静态区,所有对象共享
-
静态成员变量必须在类外初始化(类里只是声明)
-
静态成员函数没有 this 指针,所以不能访问非静态成员
-
静态成员可以通过
类名::成员或对象.成员访问 -
静态成员也受访问限定符(public/private)限制
2. 代码示例
【示例 1】统计创建了多少个对象
这个经典例子完美展示了 static 的用法!
cpp
#include <iostream>
using namespace std;
class A {
public:
// 构造函数
A() {
++_scount; // 每创建一个对象,计数+1
cout << "构造函数,当前对象数:" << _scount << endl;
}
// 拷贝构造函数
A(const A& t) {
++_scount; // 拷贝构造也要计数!
cout << "拷贝构造,当前对象数:" << _scount << endl;
}
// 析构函数
~A() {
--_scount; // 对象销毁,计数-1
cout << "析构函数,当前对象数:" << _scount << endl;
}
// 静态成员函数:获取当前对象数
static int GetACount() {
return _scount;
}
private:
// 类里面只是声明!
static int _scount;
};
// ✅ 静态成员变量必须在类外初始化!
int A::_scount = 0;
int main() {
cout << "初始对象数:" << A::GetACount() << endl; // 通过类名访问
A a1, a2;
A a3(a1); // 拷贝构造
cout << "\n创建3个对象后:" << endl;
cout << "通过类名访问:" << A::GetACount() << endl;
cout << "通过对象访问:" << a1.GetACount() << endl;
// cout << A::_scount << endl; // 编译报错!_scount是private的
return 0;
}
运行结果:
cpp
初始对象数:0
构造函数,当前对象数:1
构造函数,当前对象数:2
拷贝构造,当前对象数:3
创建3个对象后:
通过类名访问:3
通过对象访问:3
析构函数,当前对象数:2
析构函数,当前对象数:1
析构函数,当前对象数:0
【示例 2】静态成员函数没有 this 指针
cpp
#include <iostream>
using namespace std;
class Test {
public:
static void Func() {
// cout << _a << endl; // ❌ 报错!静态函数没有this指针,不能访问非静态成员
cout << "_s = " << _s << endl; // ✅ 可以访问静态成员
}
void NormalFunc() {
cout << "_a = " << _a << endl; // ✅ 非静态函数可以访问普通成员
cout << "_s = " << _s << endl; // ✅ 非静态函数也可以访问静态成员
}
private:
int _a = 10; // 普通成员变量
static int _s; // 静态成员变量
};
int Test::_s = 20;
int main() {
Test::Func(); // 不需要对象,直接通过类名调用静态函数
Test t;
t.NormalFunc();
return 0;
}
(四)友元
1. 基础概念解释
大白话时间:
类的封装就像你家的房子,private 成员就是你卧室里的东西,外人不能随便进。但你最好的朋友来了,你肯定会给他开门,让他随便参观。
友元就是 C++ 给你的 "开门权限"!友元可以突破访问限定符,直接访问类的私有成员。
友元分两种:
-
友元函数:一个函数成为某个类的朋友
-
友元类:整个类都成为朋友
注意事项:
-
友元是单向的:A 是 B 的朋友 ≠ B 是 A 的朋友
-
友元不能传递:A 是 B 的朋友,B 是 C 的朋友 ≠ A 是 C 的朋友
-
友元会破坏封装,不要滥用!
2. 代码示例
【示例 1】友元函数
cpp
#include <iostream>
using namespace std;
// 前置声明,告诉编译器B类存在
class B;
class A {
// 声明func函数是A的友元
friend void func(const A& aa, const B& bb);
private:
int _a1 = 1;
int _a2 = 2;
};
class B {
// 声明func函数也是B的友元
friend void func(const A& aa, const B& bb);
private:
int _b1 = 3;
int _b2 = 4;
};
// 这个函数同时是A和B的友元,可以访问两个类的私有成员!
void func(const A& aa, const B& bb) {
cout << "访问A的私有成员:" << aa._a1 << ", " << aa._a2 << endl;
cout << "访问B的私有成员:" << bb._b1 << ", " << bb._b2 << endl;
}
int main() {
A aa;
B bb;
func(aa, bb);
return 0;
}
运行结果:
cpp
访问A的私有成员:1, 2
访问B的私有成员:3, 4
【示例 2】友元类
cpp
#include <iostream>
using namespace std;
class A {
// 声明B类是A的友元,B的所有成员函数都能访问A的私有成员
friend class B;
private:
int _a1 = 1;
int _a2 = 2;
};
class B {
public:
void func1(const A& aa) {
cout << "func1访问A的私有:" << aa._a1 << endl;
cout << "B自己的成员:" << _b1 << endl;
}
void func2(const A& aa) {
cout << "func2访问A的私有:" << aa._a2 << endl;
cout << "B自己的成员:" << _b2 << endl;
}
private:
int _b1 = 3;
int _b2 = 4;
};
int main() {
A aa;
B bb;
bb.func1(aa);
bb.func2(aa);
// 注意:友元是单向的!A不能访问B的私有成员
return 0;
}
(五)内部类
1. 基础概念解释
大白话时间:
如果一个类定义在另一个类的里面,里面那个就叫内部类。就像你家房子里还有个小房间。
关键特性:
-
内部类是独立的类,外部类的对象大小不包含内部类
-
内部类默认就是外部类的友元,可以直接访问外部类的私有成员
-
内部类受外部类的类域和访问限定符限制
-
如果把内部类放在 private 里,那它就是外部类的 "专属工具人",外面用不了
2. 代码示例
【示例 1】内部类基本用法
cpp
#include <iostream>
using namespace std;
class A {
private:
static int _k; // 静态成员
int _h = 1; // 普通成员
public:
// B是A的内部类
class B {
public:
void foo(const A& a) {
cout << "访问A的静态成员:" << _k << endl; // ✅ 直接访问
cout << "访问A的普通成员:" << a._h << endl; // ✅ 内部类默认是友元!
}
private:
int _b1 = 10;
};
};
// 静态成员初始化
int A::_k = 666;
int main() {
cout << "A类的大小:" << sizeof(A) << endl; // 输出4,不包含B!
// 内部类的定义方式:外部类::内部类
A::B b;
A aa;
b.foo(aa);
return 0;
}
运行结果:
cpp
A类的大小:4
访问A的静态成员:666
访问A的普通成员:1
(六)匿名对象
1. 基础概念解释
大白话时间:
我们平时定义对象都是A aa(1);,这叫有名对象,生命周期是整个作用域。
但有时候我们只需要用一下这个对象,用完就扔,那可以不用给它起名字!这就是匿名对象 :A(1);
特点:
-
匿名对象的生命周期只有当前这一行,执行完就析构
-
格式:
类名(参数) -
适合临时用一下的场景,非常方便
2. 代码示例
【示例 1】匿名对象的生命周期
cpp
#include <iostream>
using namespace std;
class A {
public:
A(int a = 0)
: _a(a)
{
cout << "A(int a) 构造,a = " << _a << endl;
}
~A() {
cout << "~A() 析构,a = " << _a << endl;
}
private:
int _a;
};
class Solution {
public:
int Sum_Solution(int n) {
return n * (n + 1) / 2;
}
};
int main() {
cout << "=== 有名对象 ===" << endl;
A aa1(1); // 有名对象,生命周期到main结束
cout << "\n=== 匿名对象 ===" << endl;
A(); // 匿名对象,这一行结束就析构
A(2); // 另一个匿名对象
cout << "\n=== 匿名对象的妙用 ===" << endl;
// 只用一次的函数,不用创建对象!直接用匿名对象调用
cout << "1+2+...+10 = " << Solution().Sum_Solution(10) << endl;
cout << "\n=== main函数结束 ===" << endl;
return 0;
}
运行结果(注意析构顺序):
cpp
=== 有名对象 ===
A(int a) 构造,a = 1
=== 匿名对象 ===
A(int a) 构造,a = 0
~A() 析构,a = 0
A(int a) 构造,a = 2
~A() 析构,a = 2
=== 匿名对象的妙用 ===
1+2+...+10 = 55
=== main函数结束 ===
~A() 析构,a = 1
看到了吗? 匿名对象用完马上就析构了,而有名对象要等到作用域结束!
(七)对象拷贝时的编译器优化
1. 基础概念解释
大白话时间:
现代编译器都很 "聪明",为了提高效率,会在不影响结果的前提下,帮我们省略一些不必要的拷贝构造。
比如你写A aa = 1;,理论上应该是:
-
用 1 构造一个临时 A 对象
-
用临时对象拷贝构造 aa
但编译器会优化成:直接用 1 构造 aa,省掉了拷贝这一步!
常见优化场景:
-
传值传参时,连续的构造 + 拷贝构造 → 优化为直接构造
-
传值返回时,连续的拷贝构造 + 拷贝构造 → 优化为一次拷贝
-
不同编译器优化程度不同,VS2022 比 VS2019 优化更激进
2. 代码示例
【示例 1】编译器优化完整演示
cpp
#include <iostream>
using namespace std;
class A {
public:
A(int a = 0)
: _a1(a)
{
cout << "A(int a) 构造" << endl;
}
A(const A& aa)
: _a1(aa._a1)
{
cout << "A(const A& aa) 拷贝构造" << endl;
}
A& operator=(const A& aa) {
cout << "A& operator= 赋值重载" << endl;
if (this != &aa) {
_a1 = aa._a1;
}
return *this;
}
~A() {
cout << "~A() 析构" << endl;
}
private:
int _a1 = 1;
};
// 传值传参
void f1(A aa) {}
// 传值返回
A f2() {
A aa;
return aa;
}
int main() {
cout << "===== 场景1:传值传参 =====" << endl;
A aa1;
f1(aa1); // 拷贝构造,无法优化
cout << endl;
cout << "===== 场景2:隐式类型转换优化 =====" << endl;
f1(1); // 构造+拷贝构造 → 优化为直接构造!
cout << endl;
cout << "===== 场景3:匿名对象传参优化 =====" << endl;
f1(A(2)); // 构造+拷贝构造 → 优化为直接构造!
cout << endl;
cout << "===== 场景4:传值返回 =====" << endl;
f2(); // 不同编译器优化程度不同
cout << endl;
cout << "===== 场景5:返回值接收优化 =====" << endl;
A aa2 = f2(); // 连续拷贝构造 → 优化!
cout << endl;
cout << "===== 场景6:赋值无法优化 =====" << endl;
aa1 = f2(); // 赋值重载,无法优化
cout << endl;
return 0;
}
VS2022 下的运行结果(优化后):
cpp
===== 场景1:传值传参 =====
A(int a) 构造
A(const A& aa) 拷贝构造
~A() 析构
===== 场景2:隐式类型转换优化 =====
A(int a) 构造 // 直接构造,没有拷贝!
~A() 析构
===== 场景3:匿名对象传参优化 =====
A(int a) 构造 // 直接构造,没有拷贝!
~A() 析构
===== 场景4:传值返回 =====
A(int a) 构造
~A() 析构
===== 场景5:返回值接收优化 =====
A(int a) 构造 // VS2022直接优化成一次构造!
~A() 析构
===== 场景6:赋值无法优化 =====
A(int a) 构造
A(const A& aa) 拷贝构造
A& operator= 赋值重载
~A() 析构
~A() 析构
~A() 析构
~A() 析构
划重点: 编译器优化是 "锦上添花",我们写代码时还是要按正常逻辑写,不要依赖优化!
总结
今天我们学习了类和对象的 7 个重要知识点:
|---------------|-------------------------------------|
| 知识点 | 核心要点 |
| 初始化列表 | 真正的初始化,引用 /const/ 无默认构造必须用,按声明顺序初始化 |
| 类型转换 | 隐式转换很方便但有风险,用 explicit 禁止不需要的转换 |
| static 成员 | 全类共享,类外初始化,静态函数无 this 指针 |
| 友元 | 突破封装的后门,单向、不传递,慎用 |
| 内部类 | 定义在类里面,默认是友元,受类域限制 |
| 匿名对象 | 生命周期只有一行,用完就扔,临时用很方便 |
| 编译器优化 | 连续的构造 + 拷贝会优化,我们按正常逻辑写就行 |
这些都是 C++ 面向对象的核心知识,一定要好好理解!下一篇我们会学习,敬请期待~
学习 C++ 一定要多动手写代码,光看是学不会的!把今天的例子都自己敲一遍运行看看,你会理解得更深刻。有问题欢迎在评论区留言哦
【从零开始学 C++】系列文章:
-
第一篇:C++ 入门基础
-
第二篇:类和对象(上)
-
第三篇:类和对象(中)
-
第四篇:类和对象(下)← 你在这里
-
第五篇:努力创作中,尽请期待!