一、再探构造函数:初始化列表的底层逻辑
之前实现构造函数时,我们习惯在函数体内给成员变量赋值,但这种方式本质是'先默认初始化,再赋值'。而是成员变量'定义初始化'的真正场所,直接决定了成员变量的初始状态。
C++ 类与对象的进阶特性涵盖初始化列表、类型转换、static 成员、友元、内部类、匿名对象及编译器优化。初始化列表确保引用和 const 成员正确初始化;explicit 关键字控制隐式转换;static 成员实现类级别共享状态;友元机制平衡封装与访问需求;内部类提供紧密耦合的封装;匿名对象简化临时调用;编译器优化减少拷贝构造开销。理解这些特性有助于编写高效安全的 C++ 代码。

之前实现构造函数时,我们习惯在函数体内给成员变量赋值,但这种方式本质是'先默认初始化,再赋值'。而是成员变量'定义初始化'的真正场所,直接决定了成员变量的初始状态。
初始化列表以冒号开头,用逗号分隔成员变量,每个成员后接括号内的初始值或表达式:
class Date {
public:
// 初始化列表:_year、_month、_day 在定义时直接初始化
Date(int year, int month, int day) : _year(year), _month(month), _day(day) {}
// 函数体可空(无额外赋值逻辑时)
private:
int _year;
int _month;
int _day;
};
以下成员变量无法通过'函数体内赋值'初始化,必须在初始化列表中指定初始值,否则编译报错:
(1)引用成员变量 引用必须在定义时绑定对象,函数体内赋值会被视为'修改引用指向'(C++ 不允许):
class A {
public:
// 正确:初始化列表绑定引用
A(int& ref) : _ref(ref) {}
private:
int& _ref; // 引用成员
};
(2)const 成员变量 const 变量必须在定义时初始化,函数体内赋值会违反'常量不可修改'规则:
class A {
public:
// 正确:const 成员在初始化列表赋值
A(int n) : _n(n) {}
private:
const int _n; // const 成员
};
(3)无默认构造的自定义类型成员
若自定义类型没有默认构造(如 Time 只有带参构造),编译器无法自动初始化,必须在初始化列表显式传参:
class Time {
public:
// Time 无默认构造(默认构造需无参或全缺省)
Time(int hour) : _hour(hour) {}
private:
int _hour;
};
class Date {
public:
// 正确:初始化列表调用 Time 的带参构造
Date(int hour, int year) : _t(hour), _year(year) {}
private:
Time _t; // 无默认构造的自定义类型成员
int _year;
};
(1)初始化顺序由'类内声明顺序'决定 成员变量在初始化列表中的顺序不影响实际初始化顺序,真正顺序是成员在类中声明的顺序。若顺序不匹配,可能导致逻辑错误:
class A {
public:
// 初始化列表顺序:_a2 在前,_a1 在后
A(int a) : _a2(a), _a1(_a2) {}
void Print() { cout << _a1 << " " << _a2 << endl; }
private:
int _a1; // 声明顺序 1:先初始化_a1
int _a2; // 声明顺序 2:后初始化_a2
};
int main() {
A aa(1);
aa.Print(); // 输出:随机值 1(_a1 用未初始化的_a2 赋值)
}
避坑建议:始终让初始化列表顺序与类内声明顺序保持一致。
(2)C++11 成员缺省值与初始化列表的配合 C++11 允许在成员声明时给'缺省值',若初始化列表未显式初始化该成员,会自动使用缺省值:
class Date {
public:
// 初始化列表未显式初始化_year、_month,使用缺省值
Date(int day) : _day(day) {}
private:
int _year = 1; // 缺省值:1
int _month = 1; // 缺省值:1
int _day; // 初始化列表显式赋值
};
int main() {
Date d(20);
d.Print(); // 输出:1-1-20
}
C++ 支持'内置类型→类类型''类类型→类类型'的隐式转换,但过度隐式转换可能导致意外逻辑,explicit 关键字可精准控制转换行为。
若类有'单个内置类型参数的构造函数',编译器会自动将该内置类型隐式转换为类对象:
class A {
public:
// 单个 int 参数的构造函数:支持 int→A 的隐式转换
A(int a1) : _a1(a1) {}
void Print() { cout << _a1 << endl; }
private:
int _a1 = 1;
};
int main() {
// 隐式转换:1→A 临时对象,再拷贝构造 aa1(编译器优化为直接构造)
A aa1 = 1;
aa1.Print(); // 输出:1
// 隐式转换:const 引用绑定临时对象(临时对象具有常性)
const A& aa2 = 2;
aa2.Print(); // 输出:2
}
在构造函数前加 explicit,会禁用上述隐式转换,仅允许'显式构造':
class A {
public:
// explicit 禁用隐式转换
explicit A(int a1) : _a1(a1) {}
private:
int _a1;
};
int main() {
A aa1 = 1; // 错误:无法隐式转换
const A& aa2 = 2; // 错误:无法隐式转换
A aa3(3); // 正确:显式构造
A aa4 = A(4); // 正确:显式构造临时对象再拷贝(允许)
}
若类 B 有'以类 A 为参数的构造函数',编译器会自动将 A 对象隐式转换为 B 对象:
class A {
public:
A(int a1) : _a1(a1) {}
int GetA1() const { return _a1; }
private:
int _a1;
};
class B {
public:
// 以 A 为参数的构造函数:支持 A→B 的隐式转换
B(const A& a) : _b(a.GetA1()) {}
void Print() { cout << _b << endl; }
private:
int _b;
};
int main() {
A aa(10);
B bb = aa; // 隐式转换:A 对象→B 对象
bb.Print(); // 输出:10
}
使用建议:仅在转换逻辑明确且必要时保留隐式转换(如 string s = "hello"),否则加 explicit 避免意外转换。
用 static 修饰的成员变量 / 函数,不属于任何对象,而是属于整个类,被所有对象共享,存储在静态区(而非对象的栈 / 堆内存)。
(1)必须在类外初始化
静态成员变量在类内仅声明,初始化需在类外(全局作用域),且不加 static:
class A {
public:
static int _scount; // 类内声明
private:
int _a; // 非静态成员(每个对象独有)
};
// 类外初始化:类型 + 类域 + 变量名,不加 static
int A::_scount = 0;
(2)所有对象共享,不占对象内存
静态成员变量不存储在对象中,sizeof 对象时不包含静态成员:
int main() {
A aa1, aa2;
aa1._scount++; // 访问静态成员:对象。静态成员
A::_scount++; // 访问静态成员:类名::静态成员(推荐)
cout << sizeof(A) << endl; // 输出:4(仅包含非静态成员_a)
}
(3)受访问限定符控制
静态成员虽属于类,但仍受 public / private 限制,私有静态成员无法在类外直接访问:
class A {
private:
static int _scount; // 私有静态成员
};
int A::_scount = 0;
int main() {
cout << A::_scount << endl; // 错误:私有成员无法访问
}
(1)没有 this 指针,仅能访问静态成员
静态成员函数 不依赖对象调用,没有隐式的 this 指针,因此无法访问非静态成员(非静态成员需通过 this 指向对象):
class A {
public:
static int GetCount() {
// 正确:访问静态成员
return _scount;
// 错误:无法访问非静态成员(无 this 指针)
// return _a;
}
private:
static int _scount;
int _a;
};
(2)调用方式:类名::函数 或 对象。函数 静态成员函数可直接通过类名调用,无需实例化对象:
int main() {
// 类名直接调用(推荐)
cout << A::GetCount() << endl;
// 对象调用(允许,但无必要)
A aa;
cout << aa.GetCount() << endl;
}
静态成员的核心场景是'共享状态管理',例如统计程序中创建的对象总数:
class A {
public:
// 构造:对象创建时计数 +1
A() { ++_scount; }
// 拷贝构造:拷贝对象也是新对象,计数 +1
A(const A& t) { ++_scount; }
// 析构:对象销毁时计数 -1
~A() { --_scount; }
// 静态函数:获取当前对象个数
static int GetObjectCount() { return _scount; }
private:
static int _scount; // 静态成员:对象计数
};
// 类外初始化计数为 0
int A::_scount = 0;
int main() {
cout << A::GetObjectCount() << endl; // 输出:0(无对象)
A a1, a2;
A a3(a1); // 拷贝构造
cout << A::GetObjectCount() << endl; // 输出:3(3 个对象)
return 0;
}
友元提供了一种'选择性打破封装'的方式,允许外部函数或类访问当前类的私有 / 保护成员,同时避免全公开带来的安全风险。但友元会增加类间耦合,需谨慎使用。
若函数需频繁访问多个类的私有成员(如 operator<< 重载),可声明为这些类的友元函数:
// 前置声明:告诉编译器 B 是类(否则 A 的友元声明无法识别 B)
class B;
class A {
// 声明 func 为友元函数:func 可访问 A 的私有成员
friend void func(const A& aa, const B& bb);
private:
int _a = 1;
};
class B {
// 声明 func 为友元函数:func 可访问 B 的私有成员
friend void func(const A& aa, const B& bb);
private:
int _b = 2;
};
// 友元函数:可直接访问 A 和 B 的私有成员
void func(const A& aa, const B& bb) {
cout << aa._a << endl; // 输出:1
cout << bb._b << endl; // 输出:2
}
友元函数规则:
friend);public / private 均可)。若类 B 需频繁访问类 A 的私有成员,可将 B 声明为 A 的友元类,此时 B 的所有成员函数都能访问 A 的私有成员:
class A {
// 声明 B 为友元类:B 的所有成员函数可访问 A 的私有成员
friend class B;
private:
int _a1 = 1;
int _a2 = 2;
};
class B {
public:
void PrintA(const A& aa) {
// 正确:B 是 A 的友元类,可访问 A 的私有成员
cout << aa._a1 << " " << aa._a2 << endl;
}
private:
int _b = 3;
};
int main() {
A aa;
B bb;
bb.PrintA(aa); // 输出:1 2
}
友元类规则:
若类 A 仅为类 B 服务(如 B 的辅助工具类),可将 A 定义在 B 的内部,称为'内部类'。内部类是独立类,仅受 B 的类域和访问限定符限制。
(1)默认是外部类的友元 内部类可直接访问外部类的私有成员(无需显式声明友元),但外部类无法直接访问内部类的私有成员:
class A {
private:
static int _k; // 外部类私有静态成员
int _h = 1; // 外部类私有非静态成员
public:
// 内部类:默认是 A 的友元
class B {
public:
void PrintA(const A& a) {
// 正确:内部类可访问外部类私有成员
cout << _k << endl; // 访问静态成员(无需对象)
cout << a._h << endl; // 访问非静态成员(需外部类对象)
}
private:
int _b = 2; // 内部类私有成员
};
};
// 外部类静态成员初始化
int A::_k = 10;
int main() {
A::B b; // 访问内部类:外部类名::内部类名
A a;
b.PrintA(a); // 输出:10 1
}
(2)不占外部类对象内存 内部类是独立类,外部类对象中不包含内部类成员,sizeof 外部类时不包含内部类:
int main() {
cout << sizeof(A) << endl; // 输出:4(仅包含 A 的非静态成员_h)
cout << sizeof(A::B) << endl; // 输出:4(包含 B 的非静态成员_b)
}
当两个类耦合度极高(如'解决方案类'与'求和辅助类'),且辅助类仅给外部类使用时,用内部类可避免全局作用域污染:
class Solution {
// 内部类:仅给 Solution 使用,外部无法访问
class Sum {
public:
Sum() { _ret += _i; ++_i; }
static int GetRet() { return _ret; }
private:
static int _i;
static int _ret;
};
public:
// 计算 1+2+...+n(利用变长数组触发 Sum 构造)
int Sum_Solution(int n) {
Sum arr[n]; // 创建 n 个 Sum 对象,触发 n 次构造(累加 1~n)
return Sum::GetRet();
}
};
// 内部类静态成员初始化
int Solution::Sum::_i = 1;
int Solution::Sum::_ret = 0;
匿名对象是'无对象名'的对象,用 类型 (实参) 定义,生命周期仅当前行,适合临时使用一次的场景(如调用单次成员函数)。
class A {
public:
A(int a = 0) : _a(a) { cout << "A(int a)" << endl; }
~A() { cout << "~A()" << endl; }
void Print() { cout << _a << endl; }
private:
int _a;
};
int main() {
// 有名对象:生命周期到 main 函数结束
A aa1(1);
// 匿名对象:生命周期仅当前行(下一行即析构)
A(2);
cout << "----------------" << endl;
// 匿名对象调用成员函数(单次使用场景)
A(3).Print(); // 输出:3(调用后立即析构)
}
输出结果(注意析构顺序):
A(int a) // aa1 构造
A(int a) // 匿名对象 A(2) 构造
~A() // A(2) 析构(生命周期结束)
----------------
A(int a) // 匿名对象 A(3) 构造
3 // Print() 输出
~A() // A(3) 析构
~A() // aa1 析构(main 结束)
匿名对象可简化'临时调用函数'的代码,避免创建无用的有名对象:
class Solution {
public:
int Sum_Solution(int n) {
// 业务逻辑...
return n * (n + 1) / 2;
}
};
int main() {
// 传统方式:创建有名对象再调用函数
Solution s;
cout << s.Sum_Solution(10) << endl;
// 匿名对象:直接调用函数,代码更简洁
cout << Solution().Sum_Solution(10) << endl;
}
现代编译器会在不影响正确性的前提下,优化对象拷贝过程,减少'构造 + 拷贝构造'的冗余步骤,提升性能。优化规则因编译器而异,但核心是'合并连续的拷贝操作'。
(1)隐式类型转换的优化
A aa = 1 本质是'构造临时对象→拷贝构造 aa',编译器会优化为'直接构造 aa':
class A {
public:
A(int a) : _a(a) { cout << "A(int a)" << endl; }
A(const A& aa) : _a(aa._a) { cout << "A(const A& aa)" << endl; }
private:
int _a;
};
int main() {
// 优化前:A(1) 构造 → 拷贝构造 aa
// 优化后:直接调用 A(int a) 构造 aa(无拷贝)
A aa = 1;
}
(2)传值返回的优化
函数 A f() 返回局部对象时,优化前会'构造局部对象→拷贝构造临时对象→拷贝构造接收对象',优化后直接'构造接收对象':
A f() {
A aa(2);
return aa;
}
int main() {
// 优化前:f() 内 aa 构造 → 拷贝临时对象 → 拷贝构造 aa2
// 优化后:直接在 aa2 的内存上构造(无拷贝)
A aa2 = f();
}
Linux 下用 g++ test.cpp -fno-elide-constructors 关闭拷贝优化,可观察未优化的拷贝过程:
# 关闭优化编译
g++ test.cpp -otest -fno-elide-constructors
# 运行程序,观察多次拷贝构造输出
./test

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online