/-------------C++初阶-------------/
《 C++发展史、命名空间、输入输出、缺省参数、函数重载 》
《 C++引用、内联函数、auto、范围 for、nullptr 》
《 C++const成员与日期类 》
在 C++ 面向对象编程中,类和对象是核心基石。除了基础的类定义与对象创建,构造函数的进阶用法、静态成员的共享机制、友元的封装突破以及内部类的设计技巧,直接决定了代码的优雅度与效率。本文将结合实例,深入拆解这些关键知识点,帮你彻底掌握类和对象的进阶应用。


📊 文章专栏:< C++ >
📋 其他专栏:< Linux > 、<数据结构 > 、<优选算法>

目录
[一、再谈构造函数: 初始化列表](#一、再谈构造函数: 初始化列表)
[1. 构造函数体赋值 vs 初始化列表](#1. 构造函数体赋值 vs 初始化列表)
[2. 必须使用初始化列表的场景](#2. 必须使用初始化列表的场景)
[3. 初始化列表的关键注意事项](#3. 初始化列表的关键注意事项)
[4. explicit 关键字:禁止隐式类型转换](#4. explicit 关键字:禁止隐式类型转换)
[二、static 成员](#二、static 成员)
[【 静态成员的核心特性】](#【 静态成员的核心特性】)
[1. 友元函数](#1. 友元函数)
[2. 友元类](#2. 友元类)
[1. 内部类的核心特性](#1. 内部类的核心特性)
[2. 代码示例](#2. 代码示例)
一、再谈构造函数: 初始化列表
1. 构造函数体赋值 vs 初始化列表
创建对象时,构造函数体中的语句本质是 "赋初值" 而非 "初始化"------ 因为初始化只能执行一次,而赋值可多次进行。
初始化列表则是真正的初始化场景,它以冒号开头,用逗号分隔成员变量,格式为**成员变量(初始值)**。例如:
cpp
class Date {
public:
// 初始化列表初始化
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
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)
, _ref(ref)
, _n(10)
{}
private:
A _aobj; // 无默认构造函数的自定义类型
int& _ref; // 引用成员
const int _n; // const成员
};
3. 初始化列表的关键注意事项
成员变量的初始化顺序由类中声明顺序 决定,与初始化列表中的顺序无关。例如:
cpp
class A
{
public:
A(int a)
: _a1(a)
, _a2(_a1)
{} // 声明顺序是_a2在前,_a1在后
void Print() { cout << _a1 << " " << _a2 << endl; }
private:
int _a2; // 先声明
int _a1; // 后声明
};
int main()
{
A a(1);
a.printf();
return 0;
}
// 输出:1 随机值(_a2先初始化时,_a1尚未赋值)
无论是否显式使用初始化列表,自定义类型成员都会先通过初始化列表完成初始化。
4. explicit 关键字:禁止隐式类型转换
接收单个参数的构造函数(含全缺省构造函数、仅第一个参数无默认值的多参数构造函数),默认支持隐式类型转换。例如:
cpp
class Date {
public:
Date(int year)
: _year(year)
{} // 单参构造函数
private:
int _year;
};
// 隐式转换:用2023构造无名对象,再赋值给d1
Date d1 = 2023;
使用**explicit修饰构造函数**,可禁止这种隐式转换,提升代码可读性:
cpp
class Date
{
public:
explicit Date(int year)
: _year(year)
{} // 禁止隐式转换
private:
int _year;
};
// 编译失败:无法进行隐式类型转换
Date d1 = 2023;
5、坑

由于用2构造出来的对象是临时的,所以想要用引用绑定的话得用const引用,const引用会延长临时变量的生命周期,直到这个引用变量销毁
**【注意】:**初始化列表和函数体内赋值并不冲突,初始化列表能处理很多函数体内赋值处理不了的场景,他两之间是先执行初始化列表,再执行函数体内赋值
二、static 成员
static 成员是属于整个类的共享资源,而非某个具体对象,常用于实现类级别的统计、共享配置等功能。
【 静态成员的核心特性】
- 静态成员变量: 需在类内声明、类外初始化(初始化时不加 static 关键字),存储在静态区;
- **静态成员函数:**无隐藏的 this 指针,只能访问静态成员,不能访问非静态成员;
- 访问方式:支持类名::静态成员 或对象.静态成员两种访问方式;
- **访问控制:**受 public、protected、private 限定符约束,private 静态成员仅类内可访问。
【面试题】:实现一个类,计算程序中创建出了多少个类对象。
通过静态成员变量统计对象创建与销毁的次数,是面试高频考点:
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; // 类外初始化
// 测试代码
void TestA() {
cout << A::GetACount() << endl; // 输出:0(无对象)
A a1, a2;
A a3(a1); // 拷贝构造
// 输出:3(3个对象存在)
cout << A::GetACount() << endl;
}
【小彩蛋】
1、静态成员函数能否调用非静态成员函数?
答:不能,因为无 this 指针,无法访问具体对象的非静态成员;
2、非静态成员函数能否调用静态成员函数?
答: 可以,静态成员属于类,同时也不依赖对象,可以直接访问。
三、友元
友元提供了一种访问类私有成员的方式,虽能提升灵活性,但会破坏封装、增加耦合度(描述不同模块之间的依赖关联程度),需谨慎使用。友元分为友元函数和友元类。
1. 友元函数
**重载operator<<和operator>>**时,因 cout/cin 需作为左操作数,无法重载为成员函数(成员函数默认 this 指针为第一个参数)。此时友元函数可解决类外访问私有成员的问题:
cpp
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 >> d._month >> d._day;
return _cin;
}
int main()
{
Date d;
cin >> d; // 输入:2024 5 20
cout << d; // 输出:2024-5-20
return 0;
}
operator<<必须传const引用,因为临时对象是右值(具有常性),非const引用无法绑定右值;若用非const引用,传入临时对象时会触发编译错误,这是C++对右值不可修改的规则限制。
【注意】:
右值和临时对象具有常性就是因为避免 "修改一个马上要销毁的对象" 这种无意义且危险的操作
友元函数的核心特点:
- 不是类的成员函数,无 this 指针;
- 可访问类的公有/ 私有 / 保护(非静态成员需要用对象去访问)成员,但不能用 const 修饰;
- 可在类内任意位置声明,不受访问限定符限制;
- 一个函数可作为多个类的友元。
2. 友元类
友元类的所有成员函数,都可访问另一个类的非公有成员。但友元关系是单向且不可传递的:
cpp
class Time
{
friend class Date; // 声明Date为Time的友元类
public:
Time(int hour = 0) : _hour(hour) {}
private:
int _hour;
};
class Date
{
public:
void SetTime(int hour)
{
// 直接访问Time的私有成员
_t._hour = hour;
}
private:
Time _t;
};
注意事项:
- 单向性: Date 可访问 Time 的所有成员(必须有Time对象才能访问非静态成员),但 Time 不能访问 Date 的所有成员;
- **不可传递:**若 C 是 B 的友元,B 是 A 的友元,C 不一定是 A 的友元;
- **不可继承:**友元关系不能被子类继承。
四、内部类
内部类是定义在另一个类内部的类,它是独立的类,不属于外部类,但天生是外部类的友元。
1. 内部类的核心特性
- 访问权限: 内部类是外部类的友元,外部类只能访问内部类的公有成员;
- **静态成员访问:**内部类可直接访问外部类的 static 成员,无需外部类对象或类名;
- 内存大小:
sizeof(外部类)仅计算外部类成员,与内部类无关; - 访问方式: 需通过**
外部类::内部类**的方式创建对象。
2. 代码示例
cpp
class A {
private:
static int k;
int h; // 私有成员
public:
class B { // 内部类,天生是A的友元
public:
void foo(const A& a) {
cout << k << endl; // 直接访问A的static成员
cout << a.h << endl;// 通过A对象访问私有成员
}
};
};
int A::k = 1; // 外部类static成员初始化
int main() {
A::B b; // 必须通过外部类::内部类创建对象
b.foo(A()); // 输出:1 和 随机值(A的h未初始化)
return 0;
}
五、对象拷贝时编译器的优化
cpp
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a)" << endl;
}
A(const A& aa)
{
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 Func1(A aa)
{
}
A Func5()
{
A aa;
return aa;
}
int main()
{
A ra1 = Func5(); // 拷贝构造+拷贝构造 ->优化为构造
cout << "==============" << endl;
A ra2;
ra2 = Func5(); //不会优化
A aa1;
Func1(aa1); // 不会优化
Func1(A(1)); // 构造+拷贝构造 ->优化为构造
Func1(1); // 构造+拷贝构造 ->优化为构造
A aa2 = 1; // 构造+拷贝构造 ->优化为构造
return 0;
}
编译器对拷贝构造的优化(比如返回值优化、复制消除),更大概率发生在"对象的构造与使用处于同一表达式(可粗略理解为"同一行代码")"的场景中;而如果拆分到多行(多个表达式),优化往往难以生效,拷贝构造函数大概率会被实际调用。
【小彩蛋】:匿名对象
⽤ 类型(实参) 定义出来的对象叫做匿名对象,相⽐之前我们定义的 类型 对象名(实参) 定义出来的叫有名对象
匿名对象**⽣命周期只在当前⼀⾏**,⼀般临时定义⼀个对象当前⽤⼀下即可,就可以定义匿名对象。
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) {
cout << "Sum_Solution" << endl;
//...
return n;
}
};
void push_back(const string & s)
{
cout << "push_back:" << s << endl;
}
int main()
{
A aa(1); // 有名对象 -- 生命周期在当前函数局部域
A(2); // 匿名对象 -- 生命周期在当前行
Solution sl;
sl.Sum_Solution(10);
Solution().Sum_Solution(20);
//A& ra = A(1); // 匿名对象具有常性
const A& ra = A(1); // const引用延长匿名对象的生命周期,生命周期在当前函数局部域
return 0;
}
六、再次理解封装
面向对象的核心是封装、继承、多态,而封装的本质是 "将现实实体抽象为代码中的类":
- **现实世界:**存在各种实体(如洗衣机、日期);
- **抽象过程:**提炼实体的属性(如洗衣机的容量、日期的年 / 月 / 日)和行为(如洗衣机的洗衣功能、日期的输出功能);
- **代码实现:**用类描述抽象结果,形成自定义类型;
- **实例化:**通过类创建对象,让计算机 "认识" 现实实体。
类是对实体的抽象描述,对象是抽象的具体实例。封装则是通过访问限定符(public/private/protected),隐藏内部实现细节,仅暴露必要的接口,保证数据安全。
