【本节目标】
- 再谈构造函数
- Static 成员
- 友元
- 内部类
- 匿名对象
- 拷贝对象时的一些编译器优化
- 再次理解封装
1. 再谈构造函数
1.1 构造函数体赋值
创建对象时,编译器通过构造函数给成员变量赋初始值,但构造函数体中的操作只能称为 "赋初值",而非 "初始化"。
- 核心区别:初始化只能执行一次,而构造函数体中可对成员变量多次赋值。
cpp
class Date
{
public:
Date(int year, int month, int day)
{
_year = year; // 赋初值(可多次执行)
_month = month;
_day = day;
_year = 2024; // 再次赋值,合法
}
private:
int _year;
int _month;
int _day;
};
1.2 初始化列表
初始化列表是成员变量真正的初始化场所,语法格式:以冒号开头,逗号分隔成员变量,每个成员后接括号包裹的初始值 / 表达式。
cpp
class Date
{
public:
// 初始化列表初始化
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
!note\] 实际上没有什么好处,只是两种写法不同用而已 !\[\[C++类和对象下篇.png\]
核心注意事项
- 每个成员变量在初始化列表中只能出现一次 (初始化仅能执行一次);
2. 以下成员必须在初始化列表中初始化:- 引用成员变量(引用必须初始化,且不能更改指向);
- const 成员变量(const 变量必须初始化,且不能修改);
- 自定义类型成员(该类无默认构造函数时,必须显式传参初始化);
cpp
class A
{
public:
A(int a) : _a(a) {} // 无默认构造函数
private:
int _a;
};
class B
{
public:
// 必须在初始化列表初始化以下成员
B(int a, int ref)
: _aobj(a) // 自定义类型A无默认构造
, _ref(ref) // 引用成员
, _n(10) // const成员
{}
private:
A _aobj;
int& _ref;
const int _n;
};
```
2. 自定义类型成员变量,无论是否显式写在初始化列表,都会**优先通过初始化列表初始化**(调用其默认构造函数);
```cpp
class Time
{
public:
Time(int hour = 0) : _hour(hour)
{
cout << "Time()" << endl; // 会被调用
}
private:
int _hour;
};
class Date
{
public:
Date(int day) {} // 未显式初始化_time,但会默认初始化
private:
int _day;
Time _t; // 自定义类型成员
};
int main()
{
Date d(1); // 输出:Time()
return 0;
}
```
3. 成员变量的初始化顺序,**取决于其在类中的声明顺序**,与初始化列表中的先后次序无关;
```cpp
class A
{
public:
A(int a)
: _a1(a)
, _a2(_a1) // 初始化顺序:_a2先声明,先初始化(此时_a1未初始化,值为随机)
{}
void Print() { cout << _a1 << " " << _a2 << endl; }
private:
int _a2; // 声明次序1
int _a1; // 声明次序2
};
int main()
{
A aa(1);
aa.Print(); // 输出:1 随机值(答案选D)
return 0;
}
```
临时笔记:
临时变量具有常性;一般要用const修饰;
### 1.3 explicit 关键字
单参构造函数(或除第一个参数外其余均有默认值的构造函数),默认支持**隐式类型转换**(用其他类型值构造临时对象,再赋值给当前对象)。<font color="#ff0000">`explicit`关键字可禁止该隐式转换,提升代码可读性。</font>
```cpp
class Date
{
public:
// 单参构造函数(无explicit,支持隐式转换)
Date(int year) : _year(year) {}
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
private:
int _year = 1;
int _month = 1;
int _day = 1;
};
void Test()
{
Date d1(2022);
d1 = 2023; // 隐式转换:2023 → 临时Date对象 → 赋值给d1
}
禁止隐式转换(加 explicit)
cpp
class Date
{
public:
// explicit修饰,禁止隐式类型转换
explicit Date(int year) : _year(year) {}
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
private:
int _year = 1;
int _month = 1;
int _day = 1;
};
void Test()
{
Date d1(2022);
// d1 = 2023; // 编译报错:禁止隐式转换
}
扩展场景多参数但部分有默认值的构造函数,也支持隐式转换,
explicit同样可禁止:
cppexplicit Date(int year, int month = 1, int day = 1) : _year(year), _month(month), _day(day) {}
2. Static 成员
2.1 概念
声明为static的类成员,称为静态成员:
- 静态成员变量:用
static修饰的成员变量(类内声明,类外初始化); - 静态成员函数:用
static修饰的成员函数(无隐藏this指针)。
经典面试题:统计类对象创建个数
cpp
class A
{
public:
A() { ++_scount; } // 构造函数:创建对象+1
A(const A& t) { ++_scount; } // 拷贝构造:创建对象+1
~A() { --_scount; } // 析构函数:销毁对象-1
static int GetACount() { return _scount; } // 静态成员函数:获取对象个数
private:
static int _scount; // 静态成员变量:统计对象个数(类内声明)
};
int A::_scount = 0; // 类外初始化:不加static关键字
void TestA()
{
cout << A::GetACount() << endl; // 输出:0(无对象)
A a1, a2;
A a3(a1); // 拷贝构造创建对象
cout << A::GetACount() << endl; // 输出:3(3个对象)
}
2.2 核心特性
-
静态成员为所有类对象共享,不属于某个具体对象,存储在静态区;
-
静态成员变量必须在类外定义 ,定义时不添加
static关键字(类内仅声明); -
访问方式:
类名::静态成员或对象.静态成员(两种均可);cppint main() { A a; cout << A::GetACount() << endl; // 类名访问 cout << a.GetACount() << endl; // 对象访问 return 0; } -
静态成员函数无隐藏
this指针,不能访问任何非静态成员(非静态成员依赖具体对象); -
静态成员受访问限定符(
public/protected/private)限制(如私有静态成员,类外无法访问)。
思考问题(面试高频)
-
静态成员函数可以调用非静态成员函数吗?
→ 不能(无
this指针,无法访问非静态成员); -
非静态成员函数可以调用静态成员函数吗?
→ 可以(静态成员属于类,无需具体对象,可直接调用)。
3. 友元
友元是突破类封装的特殊机制,允许外部函数 / 类直接访问类的私有 / 保护成员,但会增加耦合度、破坏封装,不宜多用。
友元分为:友元函数、友元类。
3.1 友元函数
问题背景
重载operator<<(输出流运算符)时,若作为成员函数,this指针会抢占第一个参数位置(左操作数),导致调用形式不符合常规(如d1 << cout)。因此需重载为全局函数,但全局函数无法访问类的私有成员,需通过友元解决。
友元函数定义
类内声明全局函数,加friend关键字,该函数即可直接访问类的私有 / 保护成员(不属于类,仍是普通全局函数)。
cpp
class Date
{
// 声明友元函数:operator<<和operator>>
friend ostream& operator<<(ostream& _cout, const Date& d);
friend istream& operator>>(istream& _cin, Date& d);
public:
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
// 友元函数实现(类外,无需加friend)
ostream& operator<<(ostream& _cout, const Date& d)
{
_cout << d._year << "-" << d._month << "-" << d._day;
return _cout;
}
istream& operator>>(istream& _cin, Date& d)
{
_cin >> d._year >> d._month >> d._day;
return _cin;
}
// 调用示例
int main()
{
Date d;
cin >> d; // 正常调用:istream& operator>>(cin, d)
cout << d << endl; // 正常调用:ostream& operator<<(cout, d)
return 0;
}
友元函数特性
- 可访问类的私有 / 保护成员,但不是类的成员函数;
- 不能用
const修饰(无this指针,const修饰无意义); - 类内声明位置不受访问限定符限制(
public/private均可); - 一个函数可作为多个类的友元函数;
- 调用方式与普通全局函数一致。
3.2 友元类
友元类的所有成员函数,均为另一个类的友元函数,可直接访问其非公有成员。
核心特性
- 友元关系单向:A 是 B 的友元,不代表 B 是 A 的友元;
- 友元关系不可传递:A 是 B 的友元,B 是 C 的友元,不代表 A 是 C 的友元;
- 友元关系不可继承(后续继承章节详解)。
cpp
class Time
{
friend class Date; // 声明Date为Time的友元类:Date的所有成员可访问Time的私有成员
public:
Time(int hour = 0, int minute = 0, int second = 0)
: _hour(hour)
, _minute(minute)
, _second(second)
{}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
void SetTimeOfDate(int hour, int minute, int second)
{
// 直接访问Time的私有成员(因Date是Time的友元类)
_t._hour = hour;
_t._minute = minute;
_t._second = second;
}
private:
int _year;
int _month;
int _day;
Time _t;
};
4. 内部类
概念
一个类定义在另一个类的内部,称为内部类(嵌套类)。内部类是独立的类,不属于外部类,外部类也无特殊访问权限,但内部类天生是外部类的友元。
核心特性
- 内部类可定义在外部类的
public/protected/private区域,访问权限受对应限定符限制; - 内部类可直接访问外部类的静态成员(无需外部类对象 / 类名);
- 内部类访问外部类的非静态成员,需通过外部类的对象参数(因无
this指针); sizeof(外部类)仅计算外部类自身成员大小,与内部类无关(内部类独立存储)。
cpp
class A
{
private:
static int k; // 静态成员
int h; // 非静态成员
public:
class B // 内部类:天生是A的友元
{
public:
void foo(const A& a)
{
cout << k << endl; // 直接访问A的静态成员
cout << a.h << endl; // 通过A的对象访问非静态成员
}
};
};
int A::k = 1; // 外部类静态成员初始化
int main()
{
A::B b; // 访问内部类:外部类名::内部类名
b.foo(A()); // 传递外部类匿名对象,供内部类访问非静态成员
return 0;
}
5. 匿名对象
概念
无对象名的临时对象,语法:类名(参数)。生命周期仅当前行(行结束后自动调用析构函数),适用于仅需单次调用成员函数的场景。
cpp
class A
{
public:
A(int a = 0) : _a(a)
{
cout << "A(int a)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
class Solution {
public:
int Sum_Solution(int n) {
return n + (n > 1 ? Sum_Solution(n - 1) : 0);
}
};
int main()
{
A aa1; // 普通对象:生命周期到main函数结束
A(); // 匿名对象:生命周期仅当前行(输出A(int a) → ~A())
A aa2(2); // 普通对象
// 匿名对象的实用场景:单次调用成员函数
Solution().Sum_Solution(10); // 无需定义Solution对象,直接调用函数
return 0;
}
!warning\] 注意避免误写为`A aa1();`(编译器会解析为函数声明,而非对象创建),匿名对象直接写`A();`即可。
6. 拷贝对象时的编译器优化
编译器在传参、传返回值过程中,会对连续的构造 + 拷贝构造操作进行优化,减少对象拷贝次数(不同编译器优化力度可能不同,以 g++/VS 为主)。
优化示例(基于以下类)
cpp
class A
{
public:
A(int a = 0) : _a(a)
{
cout << "A(int a)" << endl; // 构造函数
}
A(const A& aa) : _a(aa._a)
{
cout << "A(const A& aa)" << endl; // 拷贝构造函数
}
A& operator=(const A& aa)
{
cout << "A& operator=(const A& aa)" << endl; // 赋值重载
if (this != &aa)
_a = aa._a;
return *this;
}
~A()
{
cout << "~A()" << endl; // 析构函数
}
private:
int _a;
};
不同场景的优化效果
场景 1:传值传参(无优化)
cpp
void f1(A aa) {} // 传值:拷贝构造aa
int main()
{
A aa1; // 构造:A(int a)
f1(aa1); // 拷贝构造:A(const A& aa)
// 析构顺序:aa(f1参数)→ aa1
return 0;
}
输出顺序:A (int a) → A (const A& aa) → ~A () → ~A ()
场景 2:传值返回(优化为直接构造)
cpp
A f2()
{
A aa; // 构造:A(int a)
return aa; // 若不优化:拷贝构造临时对象;优化后:直接构造临时对象
}
int main()
{
f2(); // 优化后输出:A(int a) → ~A()
return 0;
}
优化效果:连续构造 + 拷贝构造 → 直接构造(减少 1 次拷贝)
场景 3:隐式类型转换(优化为直接构造)
cpp
void f1(A aa) {}
int main()
{
f1(1); // 1→临时A对象(构造)→ 传参(拷贝构造);优化后:直接构造aa
// 优化后输出:A(int a) → ~A()
return 0;
}
优化效果:隐式构造 + 拷贝构造 → 直接构造(减少 1 次拷贝)
场景 4:表达式内连续拷贝(优化为 1 次拷贝)
cpp
int main()
{
A aa2 = f2(); // f2返回临时对象(拷贝构造)→ aa2(拷贝构造);优化后:1次拷贝
// 优化后输出:A(int a) → ~A()
return 0;
}
优化效果:连续拷贝构造 + 拷贝构造 → 1 次拷贝构造(减少 1 次拷贝)
场景 5:拷贝构造 + 赋值重载(无优化)
cpp
int main()
{
A aa1;
aa1 = f2(); // f2返回临时对象(拷贝构造)→ 赋值给aa1(赋值重载);无优化
// 输出:A(int a) → A(int a) → A& operator=(...) → ~A() → ~A()
return 0;
}
结论:赋值重载无法优化,仅连续构造 / 拷贝构造可优化。
7. 再次理解封装
计算机无法直接识别现实世界的实体,需通过 "抽象→描述→实例化" 的流程,将实体转化为计算机可识别的对象:
- 抽象:对现实实体的属性(如洗衣机的容量、品牌)和行为(洗衣、甩干)进行提炼;
- 描述:用面向对象语言(C++)将抽象结果定义为 "类"(自定义类型);
- 实例化:通过类创建具体对象,计算机即可通过对象模拟现实实体的行为。
| 现实世界 | 计算机世界 | 核心动作 |
|---|---|---|
| 实体(洗衣机、人) | 对象 | 实例化 |
| 实体的抽象特征 | 类(自定义类型) | 描述 |
核心本质:类是对一类实体的 "模板化描述",对象是模板的 "具体实例",封装则是将实体的属性和行为绑定在类中,通过访问限定符控制外部交互接口。