跳到主要内容
C++ 类和对象进阶:初始化列表、静态成员、友元与内部类详解 | 极客日志
C++
C++ 类和对象进阶:初始化列表、静态成员、友元与内部类详解 讲解 C++ 类和对象的进阶特性。涵盖构造函数初始化列表与函数体内赋值的区别,明确 const 和引用成员必须初始化。介绍 explicit 关键字避免隐式类型转换。解析 static 静态成员变量的声明、初始化及访问规则,区分静态与非静态成员函数。阐述友元函数和友元类实现跨类访问私有成员的机制及限制。说明内部类的定义及其对外部类成员的访问权限。最后简述匿名对象的生命周期特征。
人间过客 发布于 2026/3/28 更新于 2026/6/13 32 浏览1. 回看构造函数(初始化列表)
1.1 函数体内赋值
这是初学者常用的给成员变量赋值的写法。
用官方的话来说就是,在创建对象时,编译器通过调用构造函数,给对象中各成员变量一个合适的初始值。
例如:
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++ 的本贾尼大佬,把这个放在构造函数的一个地方,叫做"初始化列表"。
1.2 初始化列表
语法如下:
以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
class Date {
public :
Date (int year = 1 , int month = 1 , int day = 1 )
:_year(year), _month(month), _day(day) {}
:
_year;
_month;
_day;
};
private
int
int
int
初始化列表的语法十分简单,但请注意,初始化列表只能出现在构造函数中!
1.3 初始化列表的意义 有的读者会问,初始化列表的效果和函数体内赋值一样,能否不使用初始化列表?
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;
};
如果对 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 ;
};
当然,如果我们在初始化列表中已经有我们给变量的值了,即使在之前给了变量缺省值,编译器也会使用你在初始化列表给定的值!
1.4 初始化列表的特点
成员变量至多只能在初始化列表中出现一次(初始化至多只能初始化一次)
类中如果包含以下成员就必须要放在初始化列表位置进行初始化: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;
};
这个记忆的方式也很简单,大家可以观察以下这三种类型变量的特点,在声明之初就得初始化,否则编译器会不让你用,那迫于无奈你只能选择初始化列表这条道路了。
尽量使用初始化列表进行初始化,因为不管你是否使用初始化列表,对于自定义类型来说,一定会先使用初始化列表进行初始化
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 就是一个随机值。
1.5 explicit 关键字 在讲这个关键字之前,我先带着大家看看一些神奇的事情
1.5.1 关于内置类型向自定义类型的隐式转换 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 中的构造函数需要传递两个参数,但是隐式类型转换的过程中只能传递一个参数。由此可见,自定义类型和内置类型的隐式类型转换是需要有一定的条件的。
构造函数只有一个参数
构造函数有多个参数,除第一个参数没有默认值外,其余参数都有默认值
全缺省构造函数
所以做一个总结:构造函数不仅可以构造与初始化对象,对于接收单个参数的构造函数,还具有类型转换的作用。
1.5.2 explicit 关键字的作用 我们可能会遇到在某些场景中,不希望上述隐式类型转换的事情发生。那我们就要使用 explicit 关键字!
explicit 关键字的作用:禁止构造函数的隐式类型转换。
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;
int _month;
int _day;
};
void Test () {
Date d1 (2022 ) ;
d1 = 2023 ;
}
2. static 成员变量
2.1 概念 声明为 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;
}
2.2 静态成员的特性
静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
静态成员变量必须在类外定义,定义时不添加 static 关键字,类中只是声明
类静态成员即可用 类名::静态成员 或者 对象。静态成员 来访问
静态成员函数没有隐藏的 this 指针,不能访问任何非静态成员
静态成员也是类的成员,受 public、protected、private 访问限定符的限制
静态成员函数可以调用非静态成员函数吗?非静态成员函数可以调用类的静态成员函数吗?
3. 友元 友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
3.1 友元函数 问题:现在尝试去重载 operator<<,然后发现没办法将 operator<< 重载成成员函数。==因为 cout 的输出流对 象和隐含的 this 指针在抢占第一个参数的位置。==this 指针默认是第一个参数也就是左操作数了。但是实际使用中 cout 需要是第一个形参对象,才能正常使用。所以要将 operator<< 重载成全局函数。但又会导致类外没办法访问成员,此时就需要友元来解决。operator>>同理。
class Date {
public :
Date (int year, int month, int day)
:_year(year), _month(month), _day(day) {}
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 ;
}
友元函数可访问类的私有和保护成员,但不是类的成员函数
友元函数不能用 const 修饰
友元函数可以在类定义的任何地方声明,不受类访问限定符限制
一个函数可以是多个类的友元函数
友元函数的调用与普通函数的调用原理相同
3.2 友元类 友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
友元关系是单向的,不具有交换性。 比如上述 Time 类和 Date 类,在 Time 类中声明 Date 类为其友元类,那么可以在 Date 类中直接访问 Time 类的私有成员变量,但想在 Time 类中访问 Date 类中私有的成员变量则不行。
友元关系不能传递 如果 B 是 A 的友元,C 是 B 的友元,则不能说明 C 时 A 的友元。
友元关系不能继承,在继承位置再给大家详细介绍
class 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) {}
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 ;
};
4. 内部类
概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
注意:内部类天生就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
4.1 内部类的特性
内部类可以定义在外部类的 public、protected、private 都是可以的。
注意内部类可以直接访问外部类中的 static 成员,不需要外部类的对象/类名。
sizeof(外部类)=外部类,和内部类没有任何关系。
class A {
private :
static int k;
int h;
public :
class B
{
public :
void foo (const A& a) {
cout << k << endl;
cout << a.h << endl;
}
};
};
int A::k = 1 ;
int main () {
A::B b;
b.foo (A ());
return 0 ;
};
5. 匿名对象 匿名对象十分的常用,这里我会给大家再讲一下,如何使用匿名对象以及匿名对象的一些特点。
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 ;
}
匿名对象的使用场景:当我们只是为了使用这个类中的某些成员函数,并不在乎类中成员变量时,我们就可以使用匿名对象。
对于普通的匿名对象来说,其的生命周期就在定义的那一行。(const 匿名对象除外)
遇到 const 匿名对象时,生命周期会被提升为当前函数的生命周期中
6. 再次理解类和对象 现实生活中的实体计算机并不认识,计算机只认识二进制格式的数据。如果想要让计算机认识现实生活中的实体,用户必须通过某种面向对象的语言,对实体进行描述,然后通过编写程序,创建对象后计算机才可以认识。比如想要让计算机认识洗衣机,就需要:
用户先要对现实中洗衣机实体进行抽象 — 即在人为思想层面对洗衣机进行认识,洗衣机有什么属性,有那些功能,即对洗衣机进行抽象认知的一个过程
经过 1 之后,在人的头脑中已经对洗衣机有了一个清醒的认识,只不过此时计算机还不清楚,想要让计 算机识别人想象中的洗衣机,就需要人通过某种面相对象的语言 (比如:C++、Java、Python 等) 将洗衣 机用类来进行描述,并输入到计算机中
经过 2 之后,在计算机中就有了一个洗衣机类,但是洗衣机类只是站在计算机的角度对洗衣机对象进行 描述的,通过洗衣机类,可以实例化出一个个具体的洗衣机对象,此时计算机才能洗衣机是什么东西。
用户就可以借助计算机中洗衣机对象,来模拟现实中的洗衣机实体了。
在类和对象阶段,大家一定要体会到,类是对某一类实体 (对象) 来进行描述的,描述该对象具有那些属性,那些方法,描述完成后就形成了一种新的自定义类型,才用该自定义类型就可以实例化具体的对象。
相关免费在线工具 Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
JSON美化和格式化 将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online