目录
[1.1 构造函数体赋值](#1.1 构造函数体赋值)
[1.2 初始化列表](#1.2 初始化列表)
[1.3 explicit 关键字](#1.3 explicit 关键字)
[二、Static 成员](#二、Static 成员)
[2.1 概念与实战](#2.1 概念与实战)
[2.2 五大特性与常见问题](#2.2 五大特性与常见问题)
[3.1 通俗概念](#3.1 通俗概念)
[3.2 友元函数](#3.2 友元函数)
[3.3 友元类](#3.3 友元类)
[4.1 通俗概念](#4.1 通俗概念)
[4.2 内部类四大核心特性](#4.2 内部类四大核心特性)
[5.1 通俗概念](#5.1 通俗概念)
[5.2 关键注意点](#5.2 关键注意点)
[6.1 通俗概念](#6.1 通俗概念)
[6.2 编译器优化核心场景](#6.2 编译器优化核心场景)
[6.3 注意点](#6.3 注意点)
[7.1 核心逻辑](#7.1 核心逻辑)
[7.2 现实世界与计算机世界的对应关系](#7.2 现实世界与计算机世界的对应关系)
[7.3 关键理解](#7.3 关键理解)
[八、练习题清单(OJ 实战)](#八、练习题清单(OJ 实战))
一、再谈构造函数
1.1 构造函数体赋值
通俗概念
构造函数体内的赋值操作,本质是 "赋初值" 而非 "初始化"------ 因为初始化只能进行一次,而构造函数体中可以对成员变量多次赋值。
cpp
#include <iostream>
using namespace std;
class Date {
public:
// 构造函数体赋值(赋初值,可多次赋值)
Date(int year, int month, int day) {
_year = year; // 第一次赋值
_month = month;
_day = day;
_year = 2024; // 第二次赋值(合法)
}
void Print() {
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main() {
Date d(2023, 5, 22);
d.Print(); // 输出:2024-5-22(第二次赋值覆盖第一次)
return 0;
}
关键注意点
- 构造函数体赋值的核心是 "修改已有变量的值",成员变量的初始化(内存分配 + 初始值)在进入函数体前已完成;
- 对于引用、const、无默认构造的自定义类型成员,无法在构造函数体中 "初始化"(只能赋值,但这些成员必须初始化一次),需用初始化列表。
1.2 初始化列表
通俗概念
初始化列表是构造函数的特殊语法,用于在成员变量初始化阶段直接赋值 (仅执行一次),语法格式:构造函数名(参数) : 成员1(值1), 成员2(值2), ... {}。
cpp
#include <iostream>
using namespace std;
// 自定义类型A:无默认构造函数(必须传参)
class A {
public:
A(int a) : _a(a) {
cout << "A(int a)" << endl;
}
private:
int _a;
};
class B {
public:
// 必须用初始化列表的三种场景:引用、const、无默认构造的自定义类型
B(int a, int ref_val)
: _aobj(a) // 自定义类型A无默认构造,必须初始化列表
, _ref(ref_val)// 引用成员必须初始化列表
, _n(10) // const成员必须初始化列表
, _x(20) // 普通成员也可初始化列表(推荐)
{
// _ref = ref_val; // 错误:引用不能赋值,只能初始化
// _n = 10; // 错误:const成员不能赋值,只能初始化
// _aobj = A(a); // 错误:A无默认构造,无法先默认初始化再赋值
}
private:
A _aobj; // 无默认构造的自定义类型
int& _ref; // 引用成员
const int _n; // const成员
int _x; // 普通成员(推荐初始化列表)
};
// 初始化顺序问题(与声明次序一致,与列表顺序无关)
class C {
public:
C(int a)
: _a1(a) // 列表顺序1:_a1先写
, _a2(_a1) // 列表顺序2:_a2后写
{}
void Print() {
cout << "_a1 = " << _a1 << ", _a2 = " << _a2 << endl;
}
private:
int _a2; // 声明顺序1:_a2先声明
int _a1; // 声明顺序2:_a1后声明
};
int main() {
B b(5, 100); // 输出:A(int a)(_aobj初始化)
C c(1);
c.Print(); // 输出:_a1 = 1, _a2 = 随机值(_a2先初始化,此时_a1未初始化)
return 0;
}
cpp
A(int a)
_a1 = 1, _a2 = 4199040(随机值)
初始化列表四大注意点
- 每个成员变量在初始化列表中只能出现一次(初始化仅能一次);
- 以下成员必须 用初始化列表初始化:
- 引用成员变量(
int& _ref); - const 成员变量(
const int _n); - 无默认构造的自定义类型成员(如 A 类对象
_aobj);
- 引用成员变量(
- 无论是否使用初始化列表,自定义类型成员变量都会先通过初始化列表初始化(默认调用其默认构造);
- 成员变量的初始化顺序 = 类中声明次序 ,与初始化列表中的先后顺序无关(如 C 类中
_a2先声明,先初始化)。
1.3 explicit 关键字
通俗概念
explicit修饰单参构造函数(或除第一个参数外其余有默认值的构造函数),可禁止构造函数的隐式类型转换,避免代码可读性问题。
cpp
#include <iostream>
using namespace std;
class Date {
public:
// explicit修饰单参构造函数,禁止隐式转换
explicit Date(int year)
: _year(year)
, _month(1)
, _day(1)
{}
// 若注释explicit,以下构造函数也支持隐式转换(多参但后两个有默认值)
// Date(int year, int month = 1, int day = 1)
// : _year(year), _month(month), _day(day)
// {}
Date& operator=(const Date& d) {
if (this != &d) {
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
void Print() {
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
void Test() {
Date d1(2022);
d1.Print(); // 输出:2022-1-1
// 隐式转换:用int 2023构造无名Date对象,再赋值给d1
// 若Date构造函数加explicit,此行编译报错;否则合法
// d1 = 2023;
// 显式转换(强制类型转换,即使加explicit也合法)
d1 = Date(2023);
d1.Print(); // 输出:2023-1-1
}
int main() {
Test();
return 0;
}
关键注意点
explicit仅修饰构造函数,作用是禁止 "用其他类型隐式转换为当前类对象";- 单参构造函数(如
Date(int year))或 "首参无默认值、其余有默认值" 的多参构造函数(如Date(int year, int month=1, int day=1)),默认支持隐式转换; - 隐式转换的本质:
d1 = 2023→ 编译器自动构造Date(2023)临时对象 → 调用赋值重载给 d1 赋值; - 开发建议:单参构造函数尽量加
explicit,避免无意识的隐式转换导致代码可读性下降。
面试小提示
常考:"初始化列表和构造函数体赋值的区别?"------ 答:1. 初始化时机:初始化列表在成员变量初始化阶段执行(仅一次),构造函数体在初始化后执行(可多次赋值);2. 适用场景:引用、const、无默认构造的自定义类型必须用初始化列表;3. 效率:初始化列表更高效(避免默认初始化 + 赋值的双重操作)。
二、Static 成员
2.1 概念与实战
通俗概念
用static修饰的类成员(变量 / 函数),属于整个类而非某个对象,所有对象共享该成员,存储在静态区(不在对象内存中)。
核心实战(面试题:统计对象创建个数)
cpp
#include <iostream>
using namespace std;
class A {
public:
// 构造函数:创建对象时计数+1
A() { ++_scount; }
// 拷贝构造函数:拷贝对象时计数+1
A(const A& t) { ++_scount; }
// 析构函数:对象销毁时计数-1
~A() { --_scount; }
// 静态成员函数:访问静态成员变量(无this指针)
static int GetACount() { return _scount; }
private:
// 静态成员变量:类内声明,类外初始化
static int _scount;
};
// 静态成员变量类外初始化(不加static关键字)
int A::_scount = 0;
void TestA() {
// 未创建对象时,计数为0(通过类名访问静态成员函数)
cout << "未创建对象:" << A::GetACount() << endl; // 输出0
A a1, a2; // 构造2个对象,计数=2
A a3(a1); // 拷贝构造1个对象,计数=3
cout << "创建3个对象:" << A::GetACount() << endl; // 输出3
// 也可通过对象访问静态成员函数(不推荐,静态成员不属于对象)
cout << "通过对象访问:" << a1.GetACount() << endl; // 输出3
}
int main() {
TestA();
// TestA函数结束,对象销毁,计数=0
cout << "函数结束后:" << A::GetACount() << endl; // 输出0
return 0;
}
cpp
未创建对象:0
创建3个对象:3
通过对象访问:3
函数结束后:0
2.2 五大特性与常见问题
静态成员五大核心特性
- 共享性:静态成员为所有类对象共享,不属于某个具体对象;
- 初始化:静态成员变量必须在类外定义 (类内仅声明),定义时不加
static; - 访问方式:支持
类名::静态成员(推荐)或对象.静态成员(不推荐); - 无 this 指针:静态成员函数没有隐藏的 this 指针,不能访问任何非静态成员(变量 / 函数);
- 访问权限:受
public/protected/private限定符控制(如 private 静态成员,类外无法访问)。
常见问题
- 静态成员函数可以调用非静态成员函数吗?答:不能。静态成员函数无 this 指针,无法访问非静态成员(非静态成员依赖具体对象)。
- 非静态成员函数可以调用静态成员函数吗?答:可以。静态成员属于类,全局唯一,非静态成员函数可通过类名或隐含访问。
面试小提示
静态成员是高频考点!常考场景:统计对象个数、实现单例模式、共享配置参数等。核心记住 "无 this 指针、类外初始化、共享性" 三大关键点。
三、友元
3.1 通俗概念
友元是突破类封装的特殊机制,允许外部函数 / 类直接访问类的私有 / 保护成员,但会增加耦合度,破坏封装,不宜多用。友元分为友元函数 和友元类。
3.2 友元函数
问题引入
重载operator<<(cout 输出)时,若作为成员函数,this 指针会抢占第一个参数位置(左操作数),导致调用形式为d1 << cout(不符合常规cout << d1),需用友元函数解决。
cpp
#include <iostream>
using namespace std;
class Date {
// 声明友元函数:允许operator<<访问私有成员
friend ostream& operator<<(ostream& _cout, const Date& d);
// 声明友元函数:允许operator>>访问私有成员
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;
};
// 友元函数:类外定义,无this指针,可直接访问私有成员
ostream& operator<<(ostream& _cout, const Date& d) {
_cout << d._year << "-" << d._month << "-" << d._day;
return _cout; // 返回cout,支持连续输出(cout << d1 << d2)
}
istream& operator>>(istream& _cin, Date& d) {
_cin >> d._year >> d._month >> d._day;
return _cin; // 返回cin,支持连续输入(cin >> d1 >> d2)
}
int main() {
Date d;
cin >> d; // 调用友元函数operator>>
cout << d << endl; // 调用友元函数operator<<
return 0;
}
cpp
输入:2024 5 22
2024-5-22
友元函数五大特性
- 友元函数是类外普通函数,不属于任何类 ,但需在类内声明(加
friend关键字); - 可直接访问类的私有 / 保护成员,无需通过对象或成员函数;
- 不能用
const修饰(无 this 指针,const 修饰的是 this); - 声明位置不受访问限定符限制(public/protected/private 均可);
- 一个函数可作为多个类的友元函数。
3.3 友元类
通俗概念
若类 B 是类 A 的友元类,则类 B 的所有成员函数都可直接访问类 A 的私有 / 保护成员,但友元关系是单向、不可传递的。
cpp
#include <iostream>
using namespace std;
class Time {
// 声明Date为Time的友元类:Date的所有成员函数可访问Time的私有成员
friend class Date;
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)
{}
// Date的成员函数可直接访问Time的私有成员
void SetTimeOfDate(int hour, int minute, int second) {
_t._hour = hour; // 直接访问Time的私有成员_hour
_t._minute = minute;// 直接访问Time的私有成员_minute
_t._second = second;// 直接访问Time的私有成员_second
}
void Print() {
cout << _year << "-" << _month << "-" << _day << " ";
cout << _t._hour << ":" << _t._minute << ":" << _t._second << endl;
}
private:
int _year;
int _month;
int _day;
Time _t; // Time类对象
};
int main() {
Date d(2024, 5, 22);
d.SetTimeOfDate(10, 30, 45);
d.Print(); // 输出:2024-5-22 10:30:45
return 0;
}
友元类三大特性
- 单向性:Time 声明 Date 为友元,Date 可访问 Time 的私有成员,但 Time 不能访问 Date 的私有成员;
- 不可传递性:若 C 是 B 的友元,B 是 A 的友元,不能推出 C 是 A 的友元;
- 不可继承性:友元关系不能被子类继承(后续继承章节详解)。
面试小提示
常考:"友元的优缺点?"------ 答:优点:突破封装,方便外部函数 / 类访问类的私有成员,简化代码;缺点:增加代码耦合度,破坏封装性,降低代码可维护性,建议少用。
四、内部类
4.1 通俗概念
定义在另一个类内部的类称为内部类,它是独立的类(不属于外部类),但天生是外部类的友元类(可访问外部类的所有成员)。
cpp
#include <iostream>
using namespace std;
class A {
private:
static int _k; // 静态成员变量
int _h; // 非静态成员变量
public:
// 内部类B:独立类,天生是A的友元
class B {
public:
void foo(const A& a) {
// 可访问A的静态成员(无需A的对象)
cout << "A::_k = " << _k << endl;
// 可访问A的非静态成员(需通过A的对象)
cout << "A::_h = " << a._h << endl;
}
};
};
// 外部类A的静态成员变量类外初始化
int A::_k = 10;
int main() {
// 内部类的访问方式:外部类名::内部类名
A::B b;
A a;
a._h = 20;
b.foo(a); // 内部类访问外部类成员
// 外部类大小与内部类无关(内部类是独立类)
cout << "sizeof(A) = " << sizeof(A) << endl; // 输出4(仅_h的大小)
cout << "sizeof(A::B) = " << sizeof(A::B) << endl; // 输出1(空类)
return 0;
}
cpp
A::_k = 10
A::_h = 20
sizeof(A) = 4
sizeof(A::B) = 1
4.2 内部类四大核心特性
- 独立性:内部类是独立类,编译后生成单独的目标文件(如
A::B编译为A_B),外部类的对象不包含内部类的成员; - 友元关系:内部类天生是外部类的友元,可访问外部类的所有成员(静态成员直接访问,非静态成员需通过外部类对象);
- 访问限定符:内部类可定义在外部类的 public/protected/private 区域,访问权限受限定符控制(如 private 内部类,类外无法访问);
- 大小无关:
sizeof(外部类)仅计算外部类自身成员的大小,与内部类无关。
五、匿名对象
5.1 通俗概念
匿名对象是无名称的类对象,语法为类名(参数),生命周期仅当前行(行结束后自动调用析构函数),适合临时使用一次的场景
cpp
#include <iostream>
using namespace std;
class A {
public:
A(int a = 0) : _a(a) {
cout << "A(int a):" << this << endl;
}
~A() {
cout << "~A():" << this << endl;
}
void Show() {
cout << "A::_a = " << _a << endl;
}
private:
int _a;
};
class Solution {
public:
int Sum_Solution(int n) {
return n * (n + 1) / 2; // 简化版求和(实际需按OJ要求实现)
}
};
int main() {
// 普通对象:生命周期到main函数结束
A aa1(10);
aa1.Show();
cout << "----------------" << endl;
// 匿名对象:生命周期仅当前行,行结束后析构
A(20); // 无名称,创建后立即析构
cout << "----------------" << endl;
// 匿名对象的实用场景:临时调用成员函数(无需定义命名对象)
int sum = Solution().Sum_Solution(10); // Solution()是匿名对象
cout << "1+2+...+10 = " << sum << endl;
return 0;
}
cpp
A(int a):0x7ffee3b558a0
A::_a = 10
----------------
A(int a):0x7ffee3b55890
~A():0x7ffee3b55890
----------------
1+2+...+10 = 55
~A():0x7ffee3b558a0
5.2 关键注意点
- 匿名对象的声明:
A()是匿名对象,A aa()是函数声明(返回 A 类型,无参),避免混淆; - 生命周期:匿名对象仅在当前行有效,行结束后自动析构,比命名对象更节省内存(临时使用场景);
- 实用场景:临时调用成员函数(如 Solution ().Sum_Solution (10))、作为函数参数 / 返回值(配合编译器优化)。
六、拷贝对象时的编译器优化
6.1 通俗概念
编译器在对象传参、传返回值时,会自动优化拷贝构造的次数(如 "连续构造 + 拷贝构造" 优化为 "直接构造"),减少对象拷贝开销。
cpp
#include <iostream>
using namespace std;
class A {
public:
A(int a = 0) : _a(a) {
cout << "A(int a):" << this << endl;
}
A(const A& aa) : _a(aa._a) {
cout << "A(const A& aa):" << this << endl;
}
A& operator=(const A& aa) {
cout << "A& operator=(const A& aa):" << this << endl;
if (this != &aa) {
_a = aa._a;
}
return *this;
}
~A() {
cout << "~A():" << this << endl;
}
private:
int _a;
};
// 场景1:传值传参(无优化,拷贝1次)
void f1(A aa) {}
// 场景2:传值返回(编译器优化后,拷贝次数减少)
A f2() {
A aa(2);
return aa;
}
int main() {
cout << "=== 场景1:传值传参 ===" << endl;
A aa1(1);
f1(aa1); // 传值,调用拷贝构造
cout << endl;
cout << "=== 场景2:传值返回 ===" << endl;
f2(); // 编译器优化:构造+拷贝构造 → 直接构造(仅1次构造)
cout << endl;
cout << "=== 场景3:隐式类型转换(优化) ===" << endl;
f1(3); // 3→A(3)(构造)+ 拷贝构造 → 优化为直接构造
cout << endl;
cout << "=== 场景4:连续拷贝构造(优化) ===" << endl;
A aa2 = f2(); // f2返回拷贝+aa2拷贝构造 → 优化为1次构造
cout << endl;
cout << "=== 场景5:拷贝构造+赋值重载(无优化) ===" << endl;
aa1 = f2(); // f2返回拷贝(1次)+ 赋值重载(1次)→ 无优化
cout << endl;
return 0;
}
cpp
=== 场景1:传值传参 ===
A(int a):0x7ffee3b558a0
A(const A& aa):0x7ffee3b55890
~A():0x7ffee3b55890
~A():0x7ffee3b558a0
=== 场景2:传值返回 ===
A(int a):0x7ffee3b55880
~A():0x7ffee3b55880
=== 场景3:隐式类型转换(优化) ===
A(int a):0x7ffee3b55870
~A():0x7ffee3b55870
=== 场景4:连续拷贝构造(优化) ===
A(int a):0x7ffee3b55860
=== 场景5:拷贝构造+赋值重载(无优化) ===
A(int a):0x7ffee3b55850
A(const A& aa):0x7ffee3b55840
A& operator=(const A& aa):0x7ffee3b55860
~A():0x7ffee3b55840
~A():0x7ffee3b55850
~A():0x7ffee3b55860
6.2 编译器优化核心场景
- 隐式类型转换:
f1(3)→ 构造A(3)+ 拷贝构造 → 优化为直接构造; - 连续构造 + 拷贝构造:
f1(A(2))→ 构造A(2)+ 拷贝构造 → 优化为直接构造; - 传值返回 + 拷贝构造:
A aa2 = f2()→ f2 返回拷贝 + aa2 拷贝构造 → 优化为1 次构造; - 不可优化场景:
aa1 = f2()→ 传值返回拷贝 + 赋值重载 → 无优化(两次操作独立)。
6.3 注意点
- 优化仅在 "同一个表达式中" 生效,跨表达式的拷贝无法优化;
- 不同编译器优化程度不同(GCC/Clang 优化较彻底,VS 部分场景优化);
- 开发建议:传参时优先用引用(
const A&),减少拷贝;返回值时可利用编译器优化,避免不必要的拷贝。
七、再次理解封装
7.1 核心逻辑
计算机无法直接识别现实世界的实体(如洗衣机、人),需通过 "抽象→描述→实例化" 三步让计算机认识:
- 抽象:在思想层面对实体进行认知(如洗衣机有属性:容量、品牌;方法:洗衣、甩干);
- 描述:用 C++ 类将抽象结果编码(属性→成员变量,方法→成员函数),形成自定义类型;
- 实例化:用类创建具体对象,计算机通过对象模拟现实实体的行为。
7.2 现实世界与计算机世界的对应关系
|--------------------|-------|-------------|
| 现实世界 | 计算机世界 | 核心操作 |
| 实体(洗衣机、人) | 对象 | 实例化(类→对象) |
| 实体的共性(洗衣机的属性 / 方法) | 类 | 抽象(实体→类) |
| 实体的行为(洗衣、计算) | 成员函数 | 调用(对象。成员函数) |
| 实体的特征(容量、年龄) | 成员变量 | 访问(对象。成员变量) |
7.3 关键理解
封装的本质是 "管理访问权限":将实体的属性和方法结合,隐藏内部实现细节(如洗衣机的电机工作原理),仅暴露必要接口(如开机键、洗衣模式),让用户无需关心内部逻辑,只需通过接口交互。
八、练习题清单(OJ 实战)
推荐的 OJ 练习题(便于巩固知识点):
- 求 1+2+...+n(禁止使用乘除法、循环、条件判断);https://www.nowcoder.com/practice/7a0da8fc483247ff8800059e12d7caf1?tpId=13&tqId=11200&tPage=3&rp=3&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking
- 计算日期到天数的转换(将年 / 月 / 日转换为当年的第几天);https://www.nowcoder.com/practice/769d45d455fe40b385ba32f97e7bcded?tpId=37&&tqId=21296&rp=1&ru=/activity/oj&qru=/ta/huawei/question-ranking
- 日期差值(计算两个日期之间的天数差);https://www.nowcoder.com/practice/ccb7383c76fc48d2bbc27a2a6319631c?tpId=62&&tqId=29468&rp=1&ru=/activity/oj&qru=/ta/sju-kaoyan/question-ranking
- 打印日期(根据输入的年 / 月,打印该月的日历);https://www.nowcoder.com/practice/b1f7a77416194fd3abd63737cdfcf82b?tpId=69&&tqId=29669&rp=1&ru=/activity/oj&qru=/ta/hust-kaoyan/question-ranking
- 累加天数(给定日期和天数,计算累加后的日期)。https://www.nowcoder.com/practice/eebb2983b7bf40408a1360efb33f9e5d?tpId=40&&tqId=31013&rp=1&ru=/activity/oj&qru=/ta/kaoyan/question-ranking
提示:这些题目均为类与对象的实战场景,需结合日期类、构造函数、运算符重载等知识点实现,建议逐一完成,加深理解。
九、学习总结与建议
- 核心逻辑:本章围绕 "类的进阶特性" 展开,重点是突破基础语法,掌握构造函数优化、静态成员、友元、内部类等高级用法,同时理解编译器对拷贝的优化机制;
- 重点突破:
- 初始化列表:必须掌握三种强制场景和初始化顺序问题;
- static 成员:无 this 指针、类外初始化、共享性三大核心;
- 友元:友元函数(重载 <<>>)和友元类(单向 / 不可传递);
- 编译器优化:常见优化场景,减少拷贝开销;
- 学习方法:
- 多敲代码:重点测试初始化列表、友元、匿名对象的生命周期;
- 对比记忆:构造函数体赋值 vs 初始化列表、静态成员 vs 非静态成员、友元 vs 封装;
- 实战巩固:完成 OJ 练习题,将知识点落地到代码;
- 避坑清单:
- 初始化列表的顺序与声明顺序一致,与列表顺序无关;
- 静态成员变量必须类外初始化,不加 static;
- 友元关系是单向的,不可传递、不可继承;
- 匿名对象
A()与函数声明A aa()的区别; - explicit 禁止单参构造函数的隐式转换,提升代码可读性。