C++ 类和对象进阶:初始化列表、静态成员、友元与内部类详解
讲解 C++ 类和对象的进阶特性。涵盖构造函数初始化列表与函数体内赋值的区别,明确 const 和引用成员必须初始化。介绍 explicit 关键字避免隐式类型转换。解析 static 静态成员变量的声明、初始化及访问规则,区分静态与非静态成员函数。阐述友元函数和友元类实现跨类访问私有成员的机制及限制。说明内部类的定义及其对外部类成员的访问权限。最后简述匿名对象的生命周期特征。

讲解 C++ 类和对象的进阶特性。涵盖构造函数初始化列表与函数体内赋值的区别,明确 const 和引用成员必须初始化。介绍 explicit 关键字避免隐式类型转换。解析 static 静态成员变量的声明、初始化及访问规则,区分静态与非静态成员函数。阐述友元函数和友元类实现跨类访问私有成员的机制及限制。说明内部类的定义及其对外部类成员的访问权限。最后简述匿名对象的生命周期特征。

这是初学者常用的给成员变量赋值的写法。
用官方的话来说就是,在创建对象时,编译器通过调用构造函数,给对象中各成员变量一个合适的初始值。
例如:
class Date {
public:
// 这种是在函数体内赋值的玩法
Date(int year = 1, int month = 1, int day = 1) {
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
虽然构造函数调用后对象有了初始值,但这只能称为赋初值,不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。
区别在于变量被赋值的时候是相对于声明时还是声明后。
了解了初始化和赋值的区别后,再看对象的整体初始化:
int main() {
// 这个就是成员变量的整体初始化的地方
Date d1(2024, 11, 22);
return 0;
}
那么单个成员变量初始化的地方在哪里呢?发明 C++ 的本贾尼大佬,把这个放在构造函数的一个地方,叫做"初始化列表"。
语法如下:
以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
class Date {
public:
Date(int year = 1, int month = 1, int day = 1)
:_year(year), _month(month), _day(day) {}
private:
int _year;
int _month;
int _day;
};
初始化列表的语法十分简单,但请注意,初始化列表只能出现在构造函数中!
有的读者会问,初始化列表的效果和函数体内赋值一样,能否不使用初始化列表?
看看以下场景,没有使用初始化列表能搞定吗?
class A {
public:
A(int a, char ref) {
// 如果不使用初始化列表初始化的话,会出现编译错误
_a = a;
_ref = ref;
}
private:
const int _a;
char& _ref;
};
这段代码会报错,原因就在于无论是 const 修饰的变量还是引用都有一个共同的特点,就是在变量创建之初必须初始化。
还有一种情况需要初始化列表的帮助,就是当类中有一个不带默认构造函数的类对象,此时我们就显式的传递参数来初始化。
class B {
public:
B(int a) {}
};
class A {
public:
// 初始化列表的语法:以冒号开头,逗号进行分割,括号的内容是成员变量该被赋值的值
A(int a, char ref)
:_a(a), _ref(ref), _b(1) {}
private:
const int _a;
char& _ref;
B _b; // 这个对象_b 的构造函数是要显示传递参数的,为此只能走初始化列表
};
如果对 C++ 比较了解,此时也能明白一个问题就是,在 C++11 标准发布后,引入了一个成员变量能够给缺省值这样一个玩法,那这个缺省值就相当于初始化列表中那个括号里面的值,只不过这是让编译器帮我们搞定罢了。
public:
Date(int year = 1, int month = 1, int day = 1)
:_year(2024), _month(11), _day(22) {}
// 相当于是在初始化列表中,做了这么一件事
private:
int _year = 2024; // 给成员变量一个缺省值
int _month = 11;
int _day = 22;
};
当然,如果我们在初始化列表中已经有我们给变量的值了,即使在之前给了变量缺省值,编译器也会使用你在初始化列表给定的值!
总结如下:
成员变量至多只能在初始化列表中出现一次(初始化至多只能初始化一次) 类中如果包含以下成员就必须要放在初始化列表位置进行初始化:const 成员变量、引用成员变量、自定义类型对象(且该类没有默认构造函数时)
比如下面的这段代码:
class B {
public:
B(int a) {}
};
class A {
public:
A(int a, char ref)
:_a(a), _ref(ref), _b(1) {}
private:
const int _a; // const 成员变量
char& _ref; // 引用成员变量
B _b; // 自定义类型且没有默认的构造函数
};
这个记忆的方式也很简单,大家可以观察以下这三种类型变量的特点,在声明之初就得初始化,否则编译器会不让你用,那迫于无奈你只能选择初始化列表这条道路了。
尽量使用初始化列表进行初始化,因为不管你是否使用初始化列表,对于自定义类型来说,一定会先使用初始化列表进行初始化
class Time {
public:
Time(int hour = 0)
:_hour(hour) { cout << "Time()" << endl; }
private:
int _hour;
};
class Date {
public:
Date(int day) {}
private:
int _day;
Time _t;
};
int main() {
Date d(1);
return 0;
}
这个例子就能很好的体现这个特点,我明明没有在 Date 类中的初始化列表处初始化 Time 类的对象_t,但是成员没有报错,原因就是我们虽然没有在初始化列表写,但是编译器在编译的过程中会自动去该类中的初始化列表进行初始化工作。
成员变量在类中声明次序就是其在初始化列表中初始化顺序,与初始化列表中的向后次序无关
这个点也是很多人会忽略的一个细节,接下来让大家看看如果是错误的写法会造成什么后果!
class A {
public:
A(int n)
:_a(n), _b(_a) {
cout << "_a:" << _a << endl;
cout << "_b:" << _b << endl;
}
private:
int _b;
int _a;
};
int main() {
A a(100);
return 0;
}
大家思考一下,上面的代码打印的结果是什么?
我们来揭晓一下答案:
有的人可能看到这里就会犯浑,他认为是我这形参 n 的先是初始化了成员变量_a,此时_a 的值就是 100,紧接着再用_a 的值去初始化成员变量_b,然后_b 的值也为 100。这个就是认为初始化的顺序是从_a 到_b。
如果你理解了上面的初始化列表的特点 4,你就很清楚的知道,初始化的顺序是从_b 到_a 的,所以应该先初始化_b,因为我们没有给值,编译器也不做处理,所以_b 就是一个随机值。
在讲这个关键字之前,我先带着大家看看一些神奇的事情
大家看一下这段代码会不会报错:
class A {
public:
A(int a)
:_a(a) { cout << "A(int a)" << endl; }
private:
int _a;
};
int main() {
A aa = 10;
return 0;
}
这段代码是不会报错的,大家可以先看一下结果:
我相信这是肯定有很多人处在很懵逼的状态,有人认为你这个 10 不是一个 int 类型的数,怎么能给一个自定义类型赋值呢,类型都不匹配肯定会报错的!
这个只是我们肉眼看到的现象,其实编译器在后面做了一些手脚,这个就是所谓的隐式转换。
怎么验证呢?大家可以看到它不仅没有报错,还调用了构造函数打印出了内容,这里我们就可以得到一点线索:
那此时有的人就会给出这样一个代码:
class A {
public:
A(int a, int b)
:_a(a) { cout << "A(int a)" << endl; }
private:
int _a;
int _b;
};
int main() {
A aa = 10;
return 0;
}
这个代码会报错的,因为你 A 中的构造函数需要传递两个参数,但是隐式类型转换的过程中只能传递一个参数。由此可见,自定义类型和内置类型的隐式类型转换是需要有一定的条件的。
条件如下:
所以做一个总结:构造函数不仅可以构造与初始化对象,对于接收单个参数的构造函数,还具有类型转换的作用。
我们可能会遇到在某些场景中,不希望上述隐式类型转换的事情发生。那我们就要使用 explicit 关键字!
explicit 关键字的作用:禁止构造函数的隐式类型转换。
class Date {
public:
// 1. 单参构造函数,没有使用 explicit 修饰,具有类型转换作用
// explicit 修饰构造函数,禁止类型转换---explicit 去掉之后,代码可以通过编译
explicit Date(int year)
:_year(year) {}
/*
* 2. 虽然有多个参数,但是创建对象时后两个参数可以不传递,没有使用 explicit 修饰,具有类型转 换作用
* explicit 修饰构造函数,禁止类型转换
* 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;
}
private:
int _year;
int _month;
int _day;
};
void Test() {
Date d1(2022);
// 用一个整形变量给日期类型对象赋值
// 实际编译器背后会用 2023 构造一个无名对象,最后用无名对象给 d1 对象进行赋值
d1 = 2023;
// 将 1 屏蔽掉,2 放开时则编译失败,因为 explicit 修饰构造函数,禁止了单参构造函数类型转换的作用
}
声明为 static 的类成员称为类的静态成员,用 static 修饰的成员变量,称之为静态成员变量;用 static 修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化。
这里说明一个点,就是为什么静态成员一定要在类外面初始化?
静态成员不属于某一个类单独享有的,而是属于整个类的。通俗一点来说,就是静态成员属于公共财产,不属于私有财产。那即然是整个类所共有的话,就必定不能再类里面进行初始化,因为我们之前说过,初始化列表是给普通成员变量进行初始化,这些成员变量有个特点就是都是私有财产。
我现在来演示如何对 static 成员进行初始化:
// static 成员的初始化
class A {
private:
static int _a;
};
// 在类外面进行初始化
// 方式:就是在变量名前指明类域
int A::_a = 10;
那我们又该如何去获取到静态成员变量里面的值呢?使用配套的 static 静态成员函数即可!
class A {
public:
static int GetStatic() {
return _a;
}
private:
static int _a;
};
// 在类外面进行初始化
int A::_a = 10;
int main() {
A aa;
cout << aa.GetStatic() << endl;
}
那此是可能有的读者就会疑惑,为什么不能直接写成这样?
class A {
public:
int StaticNum() {
return _a;
}
private:
static int _a;
};
// 在类外面进行初始化
int A::_a = 10;
int main() {
A aa;
cout << aa.StaticNum() << endl;
}
上述的写法是一个严重的错误。因为我们说过静态成员变量不是属于某个对象私有的,这也就意味着它没有隐含的 this 指针,如果你采用上述的写法不就自相矛盾了。
好了,有了上面的基础,我们来一道面试题:实现一个类,计算程序中创建出了多少个类对象。
class A {
public:
A() {
++_scount;
}
A(const A& t) {
++_scount;
}
~A() {
--_scount;
}
static int GetACount() {
return _scount;
}
private:
static int _scount;
};
int A::_scount = 0;
void TestA() {
cout << A::GetACount() << endl;
A a1, a2;
A a3(a1);
cout << A::GetACount() << endl;
}
有了上面的知识做铺垫,我们来回答下面两道问题:
静态成员函数可以调用非静态成员函数吗?非静态成员函数可以调用类的静态成员函数吗?
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
友元分为:友元函数和友元类
问题:现在尝试去重载 operator<<,然后发现没办法将 operator<< 重载成成员函数。==因为 cout 的输出流对 象和隐含的 this 指针在抢占第一个参数的位置。==this 指针默认是第一个参数也就是左操作数了。但是实际使用中 cout 需要是第一个形参对象,才能正常使用。所以要将 operator<< 重载成全局函数。但又会导致类外没办法访问成员,此时就需要友元来解决。operator>>同理。
class Date {
public:
Date(int year, int month, int day)
:_year(year), _month(month), _day(day) {}
// d1 << cout; -> d1.operator<<(&d1, cout); 不符合常规调用
// 因为成员函数第一个参数一定是隐藏的 this,所以 d1 必须放在<<的左侧
ostream& operator<<(ostream& _cout) {
_cout << _year << "-" << _month << "-" << _day << endl;
return _cout;
}
private:
int _year;
int _month;
int _day;
};
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加 friend 关键字。
class Date {
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;
};
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;
_cin >> d._month;
_cin >> d._day;
return _cin;
}
int main() {
Date d;
cin >> d;
cout << d << endl;
return 0;
}
说明:
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
class Time {
friend class 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) {
// 直接访问时间类私有的成员变量
_t._hour = hour;
_t._minute = minute;
_t._second = second;
}
private:
int _year;
int _month;
int _day;
Time _t;
};
概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
注意:内部类天生就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
class A {
private:
static int k;
int h;
public:
class B // B 天生就是 A 的友元
{
public:
void foo(const A& a) {
cout << k << endl; // OK
cout << a.h << endl; // OK
}
};
};
int A::k = 1;
int main() {
A::B b;
b.foo(A());
return 0;
};
匿名对象十分的常用,这里我会给大家再讲一下,如何使用匿名对象以及匿名对象的一些特点。
概念:匿名对象顾名思义就是没有变量名的对象。
class A {
public:
A(int a = 1)
:_a(a) {
cout << _a << endl;
cout << "A(int a = 1)" << endl;
}
~A() {
cout << _a << endl;
cout << "~A()" << endl;
}
private:
int _a;
};
int main() {
// 有名对象
A aa1(10);
// 匿名对象
A(100);
return 0;
}
匿名对象的使用场景:当我们只是为了使用这个类中的某些成员函数,并不在乎类中成员变量时,我们就可以使用匿名对象。
匿名对象的特点:
现实生活中的实体计算机并不认识,计算机只认识二进制格式的数据。如果想要让计算机认识现实生活中的实体,用户必须通过某种面向对象的语言,对实体进行描述,然后通过编写程序,创建对象后计算机才可以认识。比如想要让计算机认识洗衣机,就需要:
在类和对象阶段,大家一定要体会到,类是对某一类实体 (对象) 来进行描述的,描述该对象具有那些属性,那些方法,描述完成后就形成了一种新的自定义类型,才用该自定义类型就可以实例化具体的对象。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 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