C++第四讲:类和对象(下)
这一章是类和对象的进阶收尾,重点解决 "构造函数怎么写更规范"、"类的特殊成员怎么用"、"编译器背后做了什么优化" 这几个核心问题,也是面试高频考点。
一、再探构造函数:初始化列表(必学)
1. 为什么需要初始化列表
之前我们在构造函数函数体内赋值 ,但这不是真正的 "初始化",而是 "赋值"。C++ 提供了初始化列表 ,是成员变量定义时初始化的地方,效率更高,且有些场景必须用它。
2. 语法格式
cpp
类名(参数列表)
: 成员1(初始值1), 成员2(初始值2), ...
{
// 函数体(可选,用于其他逻辑)
}
3. 必须使用初始化列表的 3 种场景(必考)
✅ 引用成员变量
✅ const 成员变量
✅ 没有默认构造函数的自定义类型成员
cpp
class Time
{
public:
// 只有带参构造,没有默认构造
Time(int hour) : _hour(hour) {}
private:
int _hour;
};
class Date
{
public:
Date(int& x, int year, int month, int day)
: _year(year) // 普通成员也可以用初始化列表
, _t(12) // 必须:Time没有默认构造
, _ref(x) // 必须:引用必须初始化
, _n(10) // 必须:const必须初始化
{
// 这里不能给_t、_ref、_n赋值,会编译报错
}
private:
int _year;
Time _t; // 自定义类型,无默认构造
int& _ref; // 引用
const int _n; // const常量
};
4. 关键规则
-
初始化顺序 = 成员在类中的声明顺序,和初始化列表的先后顺序无关(高频坑)
cppclass A { public: A(int a) : _a1(a), _a2(_a1) {} // 先初始化_a2,再初始化_a1 void Print() { cout << _a1 << " " << _a2 << endl; } private: int _a2 = 2; // 先声明_a2 int _a1 = 2; // 后声明_a1 }; int main() { A aa(1); aa.Print(); // 输出:1 随机值(_a2先初始化,此时_a1还没赋值) return 0; } -
C++11 支持成员声明时给缺省值
缺省值会传给初始化列表,如果初始化列表没显式写这个成员,就用缺省值:
cppclass Date { private: int _year = 2025; // 缺省值 int _month = 5; int _day; }; -
无论是否显式写初始化列表,所有成员都会走初始化列表
-
显式写了:用列表里的值初始化
-
没写:有缺省值用缺省值,没有则内置类型随机值,自定义类型调用默认构造
-
二、隐式类型转换与 explicit
1. 什么是隐式类型转换
C++ 允许内置类型 → 类类型 的隐式转换,前提是类有对应参数的构造函数。
cpp
class A
{
public:
A(int a) : _a(a) {}
private:
int _a;
};
int main()
{
A aa1 = 1; // 隐式转换:1 → 构造临时A对象 → 拷贝构造aa1
// 编译器优化:直接构造aa1(省略拷贝)
return 0;
}
2. explicit 关键字:禁止隐式转换
在构造函数前加explicit,就只能显式构造对象,不能隐式转换:
cpp
class A
{
public:
explicit A(int a) : _a(a) {}
private:
int _a;
};
int main()
{
// A aa1 = 1; ❌ 编译报错:禁止隐式转换
A aa2(1); // ✅ 显式构造
return 0;
}
3. 多参数隐式转换(C++11)
cpp
class A
{
public:
A(int a1, int a2) : _a1(a1), _a2(a2) {}
private:
int _a1;
int _a2;
};
int main()
{
A aa = {1, 2}; // C++11支持多参数隐式转换
return 0;
}
三、static 静态成员(面试高频)
1. 静态成员变量
-
用
static修饰,属于整个类,不属于某个对象 -
所有对象共享同一份静态变量,存在静态区
-
必须在类外初始化(不能在声明时给缺省值)
cpp
class A
{
public:
A() { ++_count; }
A(const A&) { ++_count; }
~A() { --_count; }
// 静态成员函数
static int GetCount() { return _count; }
private:
static int _count; // 类内声明
};
// 类外初始化(必须写)
int A::_count = 0;
int main()
{
cout << A::GetCount() << endl; // 0
A a1, a2;
A a3(a1);
cout << A::GetCount() << endl; // 3
return 0;
}
2. 静态成员函数
-
没有
this指针 -
只能访问静态成员变量和静态成员函数(不能访问非静态,因为没有 this)
-
调用方式:
类名::静态函数()或对象.静态函数()
3. 核心特点总结
| 对比项 | 普通成员 | 静态成员 |
|---|---|---|
| 所属 | 单个对象 | 整个类 |
| 存储 | 对象内存 | 静态区 |
| 访问 | 需要对象 | 类名直接访问 |
| this 指针 | 有 | 无 |
四、友元(突破封装)
1. 作用
允许外部函数或类访问另一个类的私有 / 保护成员,是封装的 "例外"。
2. 友元函数
cpp
class Date
{
// 声明友元函数
friend ostream& operator<<(ostream& out, const Date& d);
private:
int _year = 2025;
int _month = 5;
int _day = 1;
};
// 全局函数,访问Date的私有成员
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "-" << d._month << "-" << d._day;
return out;
}
3. 友元类
cpp
class A
{
// 声明B是A的友元类
friend class B;
private:
int _a = 1;
};
class B
{
public:
void PrintA(const A& aa)
{
cout << aa._a << endl; // B可以访问A的私有成员
}
};
4. 注意事项
-
友元是单向的:A 是 B 的友元,不代表 B 是 A 的友元
-
友元不能传递:A 是 B 的友元,B 是 C 的友元,不代表 A 是 C 的友元
-
友元破坏封装,尽量少用(必要时才用,比如重载 <<和>>)
五、内部类
1. 定义
一个类定义在另一个类的内部,叫做内部类。
cpp
class A
{
public:
// B是A的内部类
class B
{
public:
void func(const A& a)
{
cout << a._h << endl; // B默认是A的友元,可以访问私有成员
}
};
private:
int _h = 10;
};
int main()
{
A::B b; // 内部类的定义方式
A aa;
b.func(aa);
return 0;
}
2. 核心特点
-
内部类是独立的类,外部类对象不包含内部类对象
cppcout << sizeof(A) << endl; // 输出4(只有_h),不包含B的大小 -
内部类默认是外部类的友元
-
受外部类的访问限定符限制:如果 B 定义在 private 下,外部就不能使用 B
六、匿名对象
1. 定义
没有名字的对象,生命周期只有当前一行。
cpp
class A
{
public:
A(int a) : _a(a) { cout << "构造" << endl; }
~A() { cout << "析构" << endl; }
void Print() { cout << _a << endl; }
private:
int _a;
};
int main()
{
A(10).Print(); // 匿名对象:构造→调用Print→析构(同一行完成)
// 下一行匿名对象已经销毁
return 0;
}
2. 使用场景
临时调用一个对象的成员函数,不需要保存对象:
cpp
// 调用Solution的Sum_Solution函数,不需要保存Solution对象
int sum = Solution().Sum_Solution(100);
七、对象拷贝的编译器优化
现代编译器会自动优化不必要的拷贝构造,提高效率。
1. 常见优化场景
场景 1:隐式类型转换
cpp
void f(A aa) {}
f(1); // 构造临时A + 拷贝构造 → 优化为直接构造
场景 2:匿名对象传参
cpp
f(A(2)); // 构造匿名对象 + 拷贝构造 → 优化为直接构造
场景 3:传值返回
cpp
A f2()
{
A aa;
return aa; // 构造aa + 拷贝构造临时对象 → 优化为直接构造返回值
}
A aa2 = f2(); // 临时对象 + 拷贝构造aa2 → 优化为直接构造aa2
2. 关闭优化(调试用)
Linux 下编译时加参数:
cpp
g++ test.cpp -fno-elide-constructors
八、本章必背核心结论
-
初始化列表是成员变量定义的地方,引用、const、无默认构造的成员必须用它
-
初始化顺序只看声明顺序,和列表顺序无关
-
explicit禁止构造函数的隐式类型转换 -
静态成员属于类,静态变量必须类外初始化,静态函数没有 this 指针
-
友元是单向的、不可传递的,破坏封装
-
匿名对象生命周期只有一行,用于临时调用
-
编译器会优化连续的构造 + 拷贝构造为直接构造