c++类和对象(下)
- [1. 构造函数的额外内容](#1. 构造函数的额外内容)
-
- [1.1 构造函数体赋值](#1.1 构造函数体赋值)
- [1.2 初始化列表](#1.2 初始化列表)
-
- 定义
- C++初始化列表注意事项
-
- [1. 每个成员变量在初始化列表中只能出现一次](#1. 每个成员变量在初始化列表中只能出现一次)
- [2. 必须使用初始化列表的特殊成员类型](#2. 必须使用初始化列表的特殊成员类型)
- [3. 核心原因:必须在创建时完成初始化](#3. 核心原因:必须在创建时完成初始化)
- 初始化列表的使用
- 成员变量初始化顺序
- explicit关键字
- 初始化列表的使用
- 成员变量初始化顺序
- [1.3 explicit关键字](#1.3 explicit关键字)
- [2. static成员](#2. static成员)
-
- [2.1 概念](#2.1 概念)
- 面试题:计算程序中创建的类对象个数
- [2.2 特性](#2.2 特性)
- 拓展
- [3. 友元](#3. 友元)
-
- [3.1 友元函数](#3.1 友元函数)
- [3.2 友元类](#3.2 友元类)
- [4. 内部类](#4. 内部类)
- [5. 匿名对象](#5. 匿名对象)
- [6. 拷贝对象时的一些编译器优化](#6. 拷贝对象时的一些编译器优化)
1. 构造函数的额外内容
1.1 构造函数体赋值
编译器调用构造函数可为对象成员变量设置合适初始值
但构造函数体中的操作并非成员变量的初始化 仅为 赋初值
核心区别:初始化仅能执行一次 构造函数体内可对成员变量进行多次赋值
代码实现
cpp
class Date{
public:
// 构造函数体赋值 函数体内部为赋初值操作
Date(int year, int month, int day){
_year = year;
_month = month;
_day = day;
// 可多次赋值 验证并非初始化
_year = 2000;
}
private:
int _year;
int _month;
int _day;
};
核心解析
对象成员变量的创建与初始化在进入构造函数体前已完成,构造函数体内部只是对已完成初始化的成员变量进行值的修改,因此该操作仅为赋初值,而非真正的初始化。
1.2 初始化列表
定义
初始化列表以冒号开始 接着是逗号分隔的成员变量列表 每个成员变量后跟随括号包裹的初始值或表达式,是 C++ 中类成员变量真正完成初始化的位置。
cpp
class Date
{
public:
// 初始化列表 成员变量在此完成真正初始化
Date(int year, int month, int day): _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
C++初始化列表注意事项
1. 每个成员变量在初始化列表中只能出现一次
初始化列表的设计初衷是确保成员变量在对象构造时仅执行一次初始化操作。这是因为初始化在C++中是一次性过程:一旦成员变量被初始化,其值就被设定,后续无法重新初始化。如果同一个成员变量在初始化列表中出现多次,编译器会报错,因为这违反了语法规则(例如,重复初始化可能导致未定义行为)。
- 核心原因:初始化操作与赋值操作不同。初始化发生在对象创建阶段,而赋值发生在构造函数体内部。初始化列表中的每个成员只能被初始化一次,以确保对象状态的确定性。
2. 必须使用初始化列表的特殊成员类型
当类包含以下成员时,必须在初始化列表中进行初始化:
- 引用成员变量(如
int& ref)。 const成员变量(如const int value)。- 自定义类型成员(如类对象),且该自定义类型没有默认构造函数。
如果这些成员不在初始化列表中初始化,编译器会报错。以下是一个简化的代码示例,演示正确用法:
cpp
#include <iostream>
class CustomType {
public:
CustomType(int val) : data(val) {} // 无默认构造函数
private:
int data;
};
class MyClass {
public:
// 必须使用初始化列表初始化特殊成员
MyClass(int a, int b, int c)
: ref(a), constValue(b), customObj(c) // 初始化列表
{
// 构造函数体内部仅用于赋值操作,不能用于初始化上述成员
}
private:
int& ref; // 引用成员
const int constValue; // const成员
CustomType customObj; // 自定义类型成员,无默认构造函数
};
3. 核心原因:必须在创建时完成初始化
这些特殊成员必须在初始化列表中初始化的根本原因是它们的语法规则要求:
- 引用成员变量:引用在声明时必须绑定到一个对象,不能在后续赋值。如果在构造函数体内尝试赋值,会违反引用语义(引用不能重新绑定)。
const成员变量 :const变量在声明时必须初始化,因为其值不可更改。构造函数体内无法进行初始化,只能赋值,但赋值操作对const变量无效。- 自定义类型成员无默认构造函数:如果自定义类型没有默认构造函数(即无参构造函数),则必须在创建时显式初始化。构造函数体内只能调用赋值运算符,但赋值可能失败(例如,如果对象不支持赋值或资源已分配)。
总之,初始化列表允许在这些成员创建时直接调用其构造函数或绑定引用,而构造函数体内部仅支持对已初始化的成员进行赋值操作(如 int 成员)。因此,对于上述特殊成员,初始化列表是唯一合法的初始化方式。
如果忽视这些规则,会导致编译错误或运行时未定义行为。建议在编写类时优先使用初始化列表,以确保代码健壮性。
初始化列表的使用
尽量使用初始化列表初始化成员变量 因为对于自定义类型成员变量 编译器会先调用初始化列表进行初始化 无论你是否显式使用它
成员变量初始化顺序
成员变量在类中声明的次序决定了其在初始化列表中的初始化顺序 与初始化列表中的书写顺序无关 例如以下代码
cpp
class A
{
public:
A(int a)
:_a1(a)
,_a2(_a1)
{}
void Print() {
cout<<_a1<<" "<<_a2<<endl;
}
private:
int _a2;
int _a1;
};
int main() {
A aa(1);
aa.Print();
}
输出结果为 1 随机值 因为成员变量声明顺序为 _a2 在前 _a1 在后 初始化时 _a2 先被初始化为未定义的 _a1 值 导致随机结果
explicit关键字
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;
int _month;
int _day;
};
void Test()
{
Date d1(2022);
d1 = 2023; // 编译失败 因为 explicit 禁止了 int 到 Date 的隐式转换
}
使用 explicit 后 赋值操作需显式构造对象 避免歧义
初始化列表的使用
尽量使用初始化列表初始化成员变量 因为对于自定义类型成员变量 编译器会先调用初始化列表进行初始化 无论你是否显式使用它
成员变量初始化顺序
成员变量在类中声明的次序决定了其在初始化列表中的初始化顺序 与初始化列表中的书写顺序无关 例如以下代码
cpp
class A
{
public:
A(int a)
:_a1(a)
,_a2(_a1)
{}
void Print() {
cout<<_a1<<" "<<_a2<<endl;
}
private:
int _a2;
int _a1;
};
int main() {
A aa(1);
aa.Print();
}
输出结果为 1 随机值 因为成员变量声明顺序为 _a2 在前 _a1 在后 初始化时 _a2 先被初始化为未定义的 _a1 值 导致随机结果
1.3 explicit关键字
核心作用
explicit关键字专用于修饰类的构造函数,核心功能是禁止构造函数触发的隐式类型转换 ,避免因编译器自动的隐式转换导致代码可读性降低、逻辑歧义,强制开发者以显式方式完成对象构造相关操作;未被explicit修饰的特定构造函数,会天然具备隐式类型转换能力。
情况1:单个参数的构造函数(单参构造函数)
cpp
class Date {
public:
// 单参构造函数 单独测试该场景时屏蔽情况2代码
// 去掉explicit则具备隐式类型转换能力 保留则禁止
explicit Date(int year) : _year(year), _month(1), _day(1) {}
// 赋值运算符重载 为类型转换后的赋值提供支持
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); // 直接构造对象 不受explicit修饰影响 始终合法
// 核心测试语句:整形值赋值给Date类型对象
d1 = 2023;
}
未加explicit修饰的行为
构造函数具备隐式类型转换能力,d1 = 2023可正常编译执行,编译器会自动完成两步隐式操作:以整形值2023为参数,隐式调用单参构造函数,创建一个无名的 Date 临时对象;调用类的赋值运算符重载,将这个无名临时对象的成员值赋值给已存在的 d1 对象。该过程无显式的对象构造代码,直接用整形给自定义类型对象赋值,代码可读性大幅降低。
加explicit修饰的效果
直接禁止上述隐式类型转换行为,d1 = 2023会编译失败;若需完成赋值,需开发者以显式方式构造临时对象,如d1 = Date(2023),让构造行为清晰可见,提升代码可读性。
情况2:多个参数且第一个参数无默认值、其余参数均有默认值的构造函数
cpp
class Date {
public:
// 多参构造函数 单独测试该场景时屏蔽情况1代码
// 第一个参数无默认值 其余均有 默认值可让创建对象时仅传首参
// 去掉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); // 直接构造对象 仅传首参即可 不受explicit修饰影响 始终合法
// 核心测试语句:整形值赋值给Date类型对象
d1 = 2023;
}
未加explicit修饰的行为
虽为多参构造函数,但因创建对象时可仅传递第一个参数(其余参数用默认值),其效果等价于单参构造函数,具备隐式类型转换能力,d1 = 2023可正常编译执行:以整形值2023为第一个参数,其余参数用默认值,隐式调用构造函数创建无名的 Date 临时对象;调用赋值运算符重载,将无名临时对象的成员值赋值给 d1 对象。
加explicit修饰的效果
与单参构造函数一致,禁止隐式类型转换行为,d1 = 2023编译失败;需显式构造临时对象完成赋值,如d1 = Date(2023)或d1 = Date(2023,10,1),让参数传递和对象构造行为显式体现。
共性补充
explicit仅对构造函数的隐式类型转换生效,直接构造对象的写法(如Date d1(2022)、Date d2(2022,10,1))不受任何影响,始终可正常编译;- 两种情况触发隐式转换的核心共性:创建对象时可仅传递一个实参完成构造,编译器因此可将单个整形值隐式转换为 Date 类型对象。### 1.3 explicit关键字
核心作用
explicit关键字专用于修饰类的构造函数,核心功能是禁止构造函数触发的隐式类型转换 ,避免因编译器自动的隐式转换导致代码可读性降低、逻辑歧义,强制开发者以显式方式完成对象构造相关操作;未被explicit修饰的特定构造函数,会天然具备隐式类型转换能力。
2. static成员
2.1 概念
声明为static的类成员称为类的静态成员,用static修饰的成员变量为静态成员变量,用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;
// 构造创建2个对象 计数+2
A a1, a2;
// 拷贝构造创建1个对象 计数+1
A a3(a1);
// 此时共3个对象 输出3
cout << A::GetACount() << endl;
}
代码核心解析
静态成员变量_scount为所有A类对象共享,独立于具体对象存在,可精准统计对象总数;
构造函数和拷贝构造函数是对象创建的唯二方式,执行时计数自增,覆盖所有对象创建场景;
析构函数执行时计数自减,对象销毁时更新总数,保证统计实时准确;
静态成员函数GetACount用于获取计数,因_scount为私有成员,通过公有静态函数提供访问接口。
2.2 特性
- 静态成员为所有类对象所共享,不属于某个具体的对象,其内存存放在静态区
- 静态成员变量必须在类外定义,定义时不添加
static关键字,类中仅做声明操作 - 类的静态成员有两种合法访问方式:类名::静态成员 或者 对象.静态成员
- 静态成员函数没有隐藏的
this指针,无法访问类的任何非静态成员变量 / 函数 - 静态成员属于类的成员,受
public、protected、private访问限定符的限制
拓展
-
问题 1 :静态成员函数可以调用非静态成员函数吗?
答案 :不能
底层逻辑 :静态成员函数无隐藏的this指针,而非静态成员函数的执行依赖this指针,通过this才能访问具体对象的非静态成员;静态成员函数无法获取指向具体对象的this指针,因此无法调用非静态成员函数。 -
问题 2 :非静态成员函数可以调用类的静态成员函数吗?
答案 :可以
底层逻辑 :静态成员函数属于类本身,为所有对象共享,其执行不依赖任何具体对象;非静态成员函数拥有隐藏的this指针,可正常访问类的静态成员,调用静态成员函数时无需传递额外参数,直接调用即可。
3. 友元
友元提供突破类封装的访问方式,可带来编程便利,但会增加代码耦合度,破坏封装特性,因此友元不宜多用。友元分为两类:友元函数和友元类。
3.1 友元函数
问题:运算符<<和>>无法重载为类成员函数的原因
重载operator<</operator>>时,若定义为类的成员函数,隐含的this指针会默认占据第一个参数位置,成为左操作数。而实际使用中,cout/cin需要作为第一个参数,才能符合cout << 对象/cin >> 对象的常规调用逻辑。若强行定义为成员函数,会导致调用形式变为对象 << cout,违背使用习惯。因此必须将其重载为全局函数。但全局函数无法访问类的私有成员,友元函数正是解决该问题的关键。operator>>同理。
成员函数重载operator<<的错误实现
cpp
class Date {
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day) {}
// 成员函数重载<< this指针为第一个参数 左操作数必须是Date对象
ostream& operator<<(ostream& _cout) {
_cout << _year << "-" << _month << "-" << _day << endl;
return _cout;
}
private:
int _year;
int _month;
int _day;
};
该实现仅能通过d << cout调用,不符合cout << d的日常使用逻辑,无实际使用价值。
友元函数实现operator<<和operator>>的正确代码
cpp
#include <iostream>
using namespace std;
class Date {
// 声明全局函数为友元函数 授权其访问类内私有成员
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator >> (istream& in, Date& d);
public:
void Print() {
cout << _year << "-" << _month << "-" << _day << endl;
}
Date(int year=0, int month=1, int day=1)
:_year(year)
,_month(month)
,_day(day)
{}
private:
// 类内给成员变量缺省值 C++11支持 非定义初始化
int _year=0;
int _month=1;
int _day=1;
};
// 全局函数 友元授权后可访问Date私有成员
ostream& operator<<(ostream& out, const Date& d) {
out << d._year << "-" << d._month << "-" << d._day << endl;
return out; // 返回out支持链式调用
}
istream& operator >> (istream& in, Date& d) {
in >> d._year >> d._month >> d._day;
return in; // 返回in支持链式调用
}
int main() {
Date d(2025, 5, 25);
d.Print();
Date d1(2025, 5, 24);
cout << d; // 编译器解析为operator<<(cout,d) 符合使用逻辑
cout << d << d1; // 链式调用 等价于operator<<(operator<<(cout,d),d1)
cin >> d >> d1; // 链式调用 等价于operator>>(operator>>(cin,d),d1)
return 0;
}
友元函数核心说明
- 友元函数可直接访问类的私有和保护成员,但本身是定义在类外部的普通函数,不属于任何类。
- 友元函数不能用
const修饰,const仅能修饰类的成员函数,用于限制this指针。 - 友元函数可在类定义的任何位置声明,不受
public、protected、private访问限定符的限制。 - 一个函数可同时声明为多个类的友元函数,获得多个类的访问授权。
- 友元函数的调用方式与普通全局函数的调用原理完全相同,无特殊规则。
3.2 友元类
核心概念
友元类的所有成员函数,都会自动成为另一个类的友元函数,可直接访问另一个类的私有和保护成员。
友元类核心特性
- 友元关系具有单向性,不具备交换性:若A是B的友元类,仅A能访问B的非公有成员,B不能访问A的非公有成员。
- 友元关系不能传递:若C是B的友元,B是A的友元,无法推出C是A的友元。
- 友元关系不能继承:继承阶段会详细讲解该特性。
友元类实现代码
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) {
// Date作为Time友元类 可直接访问Time对象的私有成员
_t._hour = hour;
_t._minute = minute;
_t._second = second;
}
private:
int _year;
int _month;
int _day;
Time _t; // Date类的Time类型成员
};
代码解析
- Time类中声明
friend class Date完成友元类授权,Date类的所有成员函数获得Time类的全访问权限。 - Date类的
SetTimeOfDate函数可直接修改Time类对象_t的私有成员_hour/_minute/_second。 - 该友元关系为单向的,Time类并非Date类的友元类,因此Time类的成员函数无法访问Date类的私有成员。# 3. 友元
友元提供突破类封装的访问方式,可带来编程便利,但会增加代码耦合度,破坏封装特性,因此友元不宜多用。友元分为两类:友元函数和友元类。
4. 内部类
概念
一个类定义在另一个类的内部,该内部的类称为内部类。内部类是独立的类,不属于外部类,无法通过外部类对象访问内部类成员。外部类对内部类无任何优越的访问权限。核心特性:内部类天生是外部类的友元类,可通过外部类对象参数访问外部类所有成员,但外部类并非内部类的友元,无法访问内部类非公有成员。
特性
- 内部类可定义在外部类的
public、protected、private任意访问限定符下。 - 内部类可直接访问外部类的
static成员,无需通过外部类对象或类名。 sizeof(外部类)的计算结果仅包含外部类成员,与内部类无任何关系。
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) {
cout << k << endl; // 直接访问A的static成员 无需类名/对象
cout << a.h << endl; // 通过A的对象参数访问A的非static私有成员
}
};
};
// 外部类static成员 类外初始化
int A::k = 1;
int main() {
// 内部类对象定义方式 外部类名::内部类名
A::B b;
// 调用内部类成员函数 传入外部类匿名对象
b.foo(A());
return 0;
}
5. 匿名对象
概念
无需显式指定对象名的类对象,称为匿名对象,属于临时对象。其生命周期仅为定义的当前行,行执行结束后会自动调用析构函数销毁。
注意点
避免形如 A aa1() 的写法,编译器会将其解析为函数声明,而非对象定义。匿名对象可直接调用类的成员函数,适合仅需一次调用成员函数的场景,简化代码。
cpp
#include <iostream>
using namespace std;
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;
}
};
int main() {
A aa1; // 普通对象
// A aa1(); // 编译器解析为函数声明 非对象定义 禁止使用
A(); // 匿名对象 本行执行结束后立即调用析构
A aa2(2); // 普通对象
// 匿名对象直接调用成员函数 无需定义命名对象 用完即销毁
Solution().Sum_Solution(10);
return 0;
}
6. 拷贝对象时的一些编译器优化
核心概念
编译器在对象传值传参和传值返回的过程中,会进行针对性优化,减少不必要的拷贝构造,提升程序效率。优化规则仅在特定表达式场景下生效。
举例代码
cpp
#include <iostream>
using namespace std;
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;
};
// 传值传参 形参为值类型 会触发拷贝构造
void f1(A aa) {}
// 传值返回 返回值为值类型 会触发拷贝构造
A f2() {
A aa;
return aa;
}
各类场景优化解析与代码测试
cpp
int main() {
// 场景1 传值传参 普通对象传值 触发一次拷贝构造
A aa1;
f1(aa1);
cout << endl;
// 场景2 传值返回 无接收对象 触发一次拷贝构造
f2();
cout << endl;
// 场景3 隐式类型转换 连续构造+拷贝构造 -> 编译器优化为直接构造
f1(1);
// 场景4 单个表达式中 显式创建临时对象+拷贝构造 -> 优化为直接构造
f1(A(2));
cout << endl;
// 场景5 单个表达式中 连续拷贝构造+拷贝构造 -> 优化为一次拷贝构造
A aa2 = f2();
cout << endl;
// 场景6 单个表达式中 连续拷贝构造+赋值运算符重载 -> 无法优化 两步均执行
aa1 = f2();
cout << endl;
return 0;
}
核心优化规则
- 单个表达式中,连续构造函数 + 拷贝构造函数,编译器会优化为一次直接构造。
- 单个表达式中,连续拷贝构造函数 + 拷贝构造函数,编译器会优化为一次拷贝构造。
- 单个表达式中,连续拷贝构造函数 + 赋值运算符重载,编译器无法优化,两步操作都会执行。
- 优化仅发生在传值传参和传值返回的场景,传引用不会触发拷贝,无优化必要。
- 优化的核心是减少临时对象的创建,避免不必要的拷贝构造和析构调用。