目录
在 C++ 面向对象编程中,类与对象是核心概念,而掌握类与对象的进阶知识,是写出高效、健壮代码的关键。本文将从构造函数的深度探索出发,依次讲解类型转换、static 成员、友元、内部类、匿名对象以及对象拷贝的编译器优化,最后结合实际案例巩固知识点,带你全面攻克 C++ 类与对象的进阶内容。
一、再探构造函数:初始化列表的核心逻辑
构造函数是创建对象时自动调用的特殊成员函数,用于初始化对象的成员变量。除了函数体内赋值,初始化列表是更高效、更灵活的初始化方式,尤其在处理特殊成员变量时不可或缺。
1. 初始化列表的基本用法
初始化列表以冒号 :
开头,后续紧跟逗号分隔的成员变量初始化项,每个成员变量后通过括号指定初始值或表达式,语法格式如下:
class 类名 {
public:
类名(参数列表) : 成员变量1(初始值1), 成员变量2(初始值2), ... {
// 函数体(可选,用于后续逻辑处理)
}
private:
成员变量1;
成员变量2;
...
};
例如,定义一个 Date
类,通过初始化列表初始化年份、月份和日期:
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 变量一旦定义就不能修改,必须在初始化列表中指定初始值。
- 无默认构造的类类型成员:若成员变量的类没有默认构造函数(即需要显式传参的构造函数),则必须在初始化列表中通过传参调用其构造函数。
以包含 Time
类成员(无默认构造)、引用成员和 const 成员的 Date
类为例:
class Time {
public:
// Time无默认构造,必须传参
Time(int hour) : _hour(hour) {}
private:
int _hour;
};
class Date {
public:
// 必须通过初始化列表初始化_t、_ref、_n
Date(int& ref, int year, int month, int day, int hour)
: _ref(ref), _n(100), _t(hour), _year(year), _month(month), _day(day) {}
private:
int _year;
int _month;
int _day;
Time _t; // 无默认构造的类类型成员
int& _ref; // 引用成员
const int _n; // const成员
};
若未在初始化列表中初始化上述三类成员,编译器会直接报错,例如提示 “必须初始化引用”“const 限定类型对象必须初始化” 等。
3. 成员变量的初始化顺序
初始化列表中成员的声明顺序决定了初始化顺序,与成员在初始化列表中的书写顺序无关。因此,建议将初始化列表的顺序与类内成员声明顺序保持一致,避免逻辑错误。
例如,以下代码中 _a2
先于 _a1
声明,因此 _a2
会先初始化(此时 _a1
未初始化,值为随机值):
class A {
public:
A(int a) : _a1(a), _a2(_a1) {} // 初始化顺序:_a2先初始化,_a1后初始化
void Print() {
cout << _a1 << " " << _a2 << endl; // 输出:1 随机值(因_a2初始化时_a1未定义)
}
private:
int _a2; // 声明顺序1:_a2先声明
int _a1; // 声明顺序2:_a1后声明
};
4. 成员变量的缺省值(C++11 特性)
C++11 支持在类内成员变量声明时指定缺省值,该缺省值仅作用于 “未在初始化列表中显式初始化” 的成员变量 —— 若成员未在初始化列表中初始化,且声明时有缺省值,则用缺省值初始化;若声明时无缺省值,内置类型成员可能为随机值(取决于编译器),自定义类型成员会调用其默认构造函数(无默认构造则报错)。
class Date {
public:
// 仅显式初始化_month,其他成员用缺省值
Date() : _month(2) {}
void Print() {
cout << _year << "-" << _month << "-" << _day << endl; // 输出:1-2-0(_day为内置类型无缺省值,可能为随机值)
}
private:
int _year = 1; // 缺省值1
int _month = 1; // 缺省值1(被初始化列表覆盖为2)
int _day; // 无缺省值(内置类型,值随机)
Time _t = 12; // 缺省值12(调用Time(int hour)构造)
const int _n = 10;// 缺省值10(const成员,无需初始化列表显式初始化)
};
5. 初始化列表的核心总结
- 无论是否显式书写初始化列表,每个构造函数都存在初始化列表,所有成员变量都会通过初始化列表完成初始化。
- 初始化列表的优先级高于缺省值:显式在初始化列表中初始化的成员,用指定值;未显式初始化的成员,用缺省值(若有);无缺省值的内置类型成员可能为随机值,自定义类型成员需有默认构造。
二、类型转换:内置类型与类类型的双向转换
C++ 支持内置类型与类类型、类类型与类类型之间的隐式转换,但需通过特定构造函数实现;若要禁止隐式转换,可使用 explicit
关键字。
1. 内置类型隐式转换为类类型
当类存在 “仅接收一个内置类型参数” 的构造函数时,C++ 允许将该内置类型值隐式转换为类对象,本质是先创建一个临时类对象,再用临时对象初始化目标对象(编译器会优化为直接构造)。
class A {
public:
// 单参数构造函数,支持int隐式转换为A
A(int a1) : _a1(a1) {}
void Print() { cout << _a1 << endl; }
private:
int _a1;
};
int main() {
A aa1 = 1; // 隐式转换:int 1 → A(1),编译器优化为直接构造
aa1.Print();// 输出:1
const A& aa2 = 2; // 隐式转换:int 2 → 临时A对象,引用绑定临时对象(需const)
aa2.Print(); // 输出:2
// C++11支持多参数隐式转换(通过初始化列表)
A aa3 = {3, 4}; // 隐式转换:{3,4} → A(3,4)(需A有双参数构造函数)
return 0;
}
2. 禁止隐式转换:explicit 关键字
在构造函数前添加 explicit
关键字,可禁止内置类型到类类型的隐式转换,仅允许显式构造(即通过 类名(参数)
直接创建对象)。
修改上述 A
类的构造函数:
class A {
public:
// explicit禁止隐式转换
explicit A(int a1) : _a1(a1) {}
...
};
int main() {
A aa1 = 1; // 编译报错:禁止隐式转换
A aa2(1); // 显式构造,正确
return 0;
}
3. 类类型之间的隐式转换
类类型之间的隐式转换,需目标类存在 “以源类对象为参数” 的构造函数(即转换构造函数)。例如,B
类存在接收 A
对象的构造函数,则 A
对象可隐式转换为 B
对象。
示例代码:
class A {
public:
A(int a1) : _a1(a1) {}
int GetA1() const { return _a1; }
private:
int _a1;
};
class B {
public:
// 转换构造函数:A → B
B(const A& a) : _b(a.GetA1()) {}
private:
int _b;
};
int main() {
A aa(5);
B bb = aa; // 隐式转换:A对象aa → B对象bb(调用B(const A&))
const B& rb = aa; // 引用绑定转换后的临时B对象(需const)
return 0;
}
三、static 成员:属于类的共享资源
用 static
修饰的成员变量和成员函数,称为静态成员,其核心特点是 “属于类,而非单个对象”,所有对象共享静态成员,存储在静态区(而非对象的内存空间中)。
1. 静态成员变量
- 类内声明,类外初始化:静态成员变量需在类内声明(添加
static
),在类外(全局作用域)初始化,且初始化时无需加static
,语法为类型 类名::静态成员变量 = 初始值
。 - 共享性:所有对象访问的是同一个静态成员变量,修改一个对象的静态成员,会影响所有对象。
- 访问方式:可通过
类名::静态成员变量
或对象.静态成员变量
访问(受访问限定符限制,如private
静态成员仅类内可访问)。
示例:统计类对象的创建个数
class A {
public:
A() { ++_scount; } // 构造函数:创建对象时计数+1
A(const A& t) { ++_scount; } // 拷贝构造:拷贝对象时计数+1
~A() { --_scount; } // 析构函数:销毁对象时计数-1
static int GetCount() { // 静态成员函数:获取计数
return _scount;
}
private:
static int _scount; // 类内声明:统计对象个数
};
// 类外初始化:静态成员变量必须在此处初始化
int A::_scount = 0;
int main() {
cout << A::GetCount() << endl; // 输出:0(无对象创建)
A a1, a2; // 创建2个对象,_scount=2
A a3(a1); // 拷贝构造1个对象,_scount=3
cout << A::GetCount() << endl; // 输出:3
cout << a1.GetCount() << endl; // 输出:3(对象也可调用静态成员函数)
// cout << A::_scount << endl; // 报错:_scount是private,类外不可访问
return 0;
}
2. 静态成员函数
- 无 this 指针:静态成员函数不属于任何对象,因此没有
this
指针,无法访问非静态成员变量和非静态成员函数(需通过对象或类名访问)。 - 访问权限:可访问静态成员变量和静态成员函数,受访问限定符限制。
- 调用方式:与静态成员变量一致,通过
类名::静态成员函数()
或对象.静态成员函数()
调用。
3. 静态成员的核心注意事项
- 静态成员变量不能在类内声明时给缺省值:缺省值是给构造函数初始化列表的,而静态成员不属于对象,不走初始化列表。
- 静态成员函数不能被 const 修饰:const 成员函数的作用是限制
this
指针不可修改,而静态成员函数无this
指针,无需 const 修饰。
四、友元:突破封装的 “特殊通道”
友元机制允许外部函数或类访问当前类的 private
和 protected
成员,是突破类封装的灵活方式,但会增加类之间的耦合度,需谨慎使用。友元分为友元函数和友元类两类。
1. 友元函数
友元函数是在类内通过 friend
声明的外部函数,其核心特点:
- 友元函数不是类的成员函数,无
this
指针,需通过参数传递类对象才能访问其成员。 - 友元声明可放在类内任意位置(
public
/private
/protected
),不受访问限定符限制。 - 一个函数可同时是多个类的友元函数。
示例:通过友元函数访问两个类的私有成员
// 前置声明:告诉编译器B是一个类,避免A的友元函数声明报错
class B;
class A {
// 声明func为A的友元函数
friend void func(const A& aa, const B& bb);
private:
int _a1 = 1;
};
class B {
// 声明func为B的友元函数
friend void func(const A& aa, const B& bb);
private:
int _b1 = 3;
};
// 友元函数:可访问A和B的私有成员
void func(const A& aa, const B& bb) {
cout << aa._a1 << endl; // 输出:1(访问A的私有成员)
cout << bb._b1 << endl; // 输出:3(访问B的私有成员)
}
int main() {
A aa;
B bb;
func(aa, bb); // 调用友元函数
return 0;
}
2. 友元类
友元类是在类内通过 friend class 类名
声明的外部类,其核心特点:
- 友元类的所有成员函数都自动成为当前类的友元函数,可访问当前类的私有成员。
- 友元关系是单向的:若 A 是 B 的友元类,A 的成员可访问 B 的私有成员,但 B 的成员不可访问 A 的私有成员。
- 友元关系不可传递:若 A 是 B 的友元,B 是 C 的友元,A 不是 C 的友元。
示例:友元类 B 访问 A 的私有成员
class A {
// 声明B为A的友元类
friend class B;
private:
int _a1 = 1;
int _a2 = 2;
};
class B {
public:
void PrintA1(const A& aa) {
cout << aa._a1 << endl; // 输出:1(B的成员函数访问A的私有成员)
}
void PrintA2(const A& aa) {
cout << aa._a2 << endl; // 输出:2(B的成员函数访问A的私有成员)
}
private:
int _b1 = 3;
};
int main() {
A aa;
B bb;
bb.PrintA1(aa); // 正确:B是A的友元类
bb.PrintA2(aa); // 正确:B是A的友元类
// cout << aa._a1 << endl; // 报错:_a1是A的私有成员,main函数不可访问
return 0;
}
五、内部类:嵌套在类中的独立类
若一个类定义在另一个类的内部,该类称为内部类(嵌套类)。内部类是独立的类,仅受外部类的类域和访问限定符限制,外部类的对象中不包含内部类的成员。
1. 内部类的基本特性
- 默认是外部类的友元:内部类的成员函数可直接访问外部类的
private
和protected
成员(需通过外部类对象或静态成员访问非静态成员)。 - 类域限制:内部类的访问需通过
外部类名::内部类名
的方式,例如A::B b;
(创建 A 的内部类 B 的对象)。 - 封装性:若内部类定义在外部类的
private
区域,则仅外部类可访问该内部类;定义在public
区域,则外部可访问。
示例:内部类访问外部类的成员
class A {
private:
static int _k; // 外部类的静态私有成员
int _h = 1; // 外部类的非静态私有成员
public:
// 内部类B,定义在public区域,外部可访问
class B {
public:
void Print(const A& a) {
cout << _k << endl; // 访问外部类的静态成员(无需对象)
cout << a._h << endl; // 访问外部类的非静态成员(需外部类对象)
}
};
};
// 外部类静态成员的类外初始化
int A::_k = 10;
int main() {
A::B b; // 通过“外部类名::内部类名”创建内部类对象
A a;
b.Print(a); // 输出:10 1(内部类访问外部类成员)
cout << sizeof(A) << endl; // 输出:4(A的大小仅包含_h,不包含B的成员)
return 0;
}
2. 内部类的使用场景
当两个类存在紧密关联,且一个类(内部类)的功能仅为另一个类(外部类)服务时,适合使用内部类。例如,Solution
类的 Sum
内部类仅用于计算累加和,无需暴露给外部:
class Solution {
// 内部类Sum,定义在private区域,仅Solution可访问
class Sum {
public:
Sum() {
_ret += _i;
++_i;
}
};
private:
static int _i;
static int _ret;
public:
int Sum_Solution(int n) {
Sum arr[n]; // 创建n个Sum对象,通过构造函数累加1~n
return _ret;
}
};
// 静态成员的类外初始化
int Solution::_i = 1;
int Solution::_ret = 0;
六、匿名对象:临时使用的 “无名称对象”
匿名对象是通过 类型(实参)
直接创建的无名称对象,其核心特点是生命周期仅在当前行,使用后立即销毁,适合临时需要一个对象且无需复用的场景。
1. 匿名对象的基本用法
匿名对象的语法格式为 类名(实参)
,例如 A(1)
(创建 A 类的匿名对象,参数为 1)。对比有名对象 A aa(1)
,匿名对象无需定义对象名,仅在当前行有效。
示例:匿名对象的创建与生命周期
class A {
public:
A(int a = 0) : _a(a) {
cout << "A(int a):构造对象" << endl;
}
~A() {
cout << "~A():销毁对象" << endl;
}
private:
int _a;
};
int main() {
A aa1(1); // 有名对象:生命周期到main函数结束
cout << "创建匿名对象:" << endl;
A(2); // 匿名对象:当前行创建,下一行立即销毁(输出构造+析构)
cout << "匿名对象已销毁" << endl;
// 匿名对象的实用场景:调用成员函数后立即销毁
Solution().Sum_Solution(10); // 创建Solution匿名对象,调用函数后销毁
return 0;
}
运行结果:
A(int a):构造对象 // aa1的构造
创建匿名对象:
A(int a):构造对象 // 匿名对象的构造
~A():销毁对象 // 匿名对象的析构(当前行结束)
匿名对象已销毁
~A():销毁对象 // aa1的析构(main函数结束)
2. 匿名对象的注意事项
- 匿名对象不能用于 “默认构造的无参场景”:
A()
会被编译器识别为函数声明(返回值为 A,无参数),而非匿名对象。若需创建无参匿名对象,可省略括号(仅 C++11 及以后支持),或传递默认参数,例如A{}
或A(0)
(若 A 的构造函数有默认参数)。
七、对象拷贝的编译器优化:提升效率的 “隐形之手”
现代 C++ 编译器会在不影响代码正确性的前提下,对对象拷贝过程(如传值传参、传值返回)进行优化,减少不必要的拷贝构造,提升程序效率。优化规则虽无统一标准,但主流编译器(如 GCC、VS)的优化逻辑基本一致。
1. 编译器优化的常见场景
(1)传值传参的优化
当通过 “内置类型值” 或 “匿名对象” 传值给函数时,编译器会将 “构造临时对象 + 拷贝构造” 优化为直接构造。
示例代码:
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&):拷贝构造" << endl; }
};
void f1(A aa) {} // 传值参数
int main() {
// 场景1:有名对象传参(无优化,调用拷贝构造)
A aa1;
f1(aa1); // 输出:A(const A&):拷贝构造
// 场景2:内置类型传参(优化为直接构造)
f1(1); // 输出:A(int a):构造(跳过临时对象的拷贝)
// 场景3:匿名对象传参(优化为直接构造)
f1(A(2)); // 输出:A(int a):构造(跳过临时对象的拷贝)
return 0;
}
(2)传值返回的优化
当函数返回对象时,编译器会根据情况优化:
- 若返回的是局部对象,且接收返回值的对象在同一表达式中定义,编译器会将 “构造局部对象 + 拷贝构造临时对象 + 拷贝构造接收对象” 优化为直接构造接收对象。
- 若返回后通过赋值运算符赋值给已有对象,则无法优化(需调用赋值重载)。
示例代码:
A f2() {
A aa; // 局部对象
return aa; // 返回局部对象
}
int main() {
// 场景1:直接接收返回值(优化为直接构造)
A aa2 = f2(); // 输出:A(int a):构造(跳过两次拷贝)
// 场景2:赋值接收返回值(无优化,调用赋值重载)
A aa3;
aa3 = f2(); // 输出:A(int a):构造(局部对象)→ A(const A&):拷贝构造(临时对象)→ 赋值重载
return 0;
}
2. 关闭编译器优化(用于调试)
在 GCC 编译器中,可通过 -fno-elide-constructors
选项关闭对象拷贝的优化,查看未优化的拷贝过程。例如:
g++ test.cpp -o test -fno-elide-constructors # 关闭优化
./test # 运行程序,查看完整的构造、拷贝构造过程
八、实战案例:日期相关工具的实现
结合上述类与对象的知识,我们可以实现实用的日期工具,例如 “计算一年中的第几天” 和 “计算两个日期的差值”。
1. 案例 1:计算一年中的第几天
通过函数封装日期逻辑,判断闰年并计算月份累计天数,最终得到当前日期是当年的第几天。
代码实现:
#include <iostream>
using namespace std;
// 获取指定年月的天数(处理闰年2月)
int GetDay(int year, int month) {
// 静态数组存储各月天数(索引0无用,对应1~12月)
static int MonthDay[13] = { -1,31,28,31,30,31,30,31,31,30,31,30,31 };
// 闰年2月为29天
if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))) {
return 29;
}
return MonthDay[month];
}
// 计算当前日期是当年的第几天
int Jisuan(int year, int month, int day) {
int sum = 0;
// 累加前month-1个月的天数
for (int i = 1; i < month; i++) {
sum += GetDay(year, i);
}
// 加上当月的天数
return sum + day;
}
int main() {
int year, month, day;
// 循环读取输入(直到EOF)
while (scanf("%d %d %d", &year, &month, &day) != EOF) {
cout << Jisuan(year, month, day) << endl;
}
return 0;
}
2. 案例 2:计算两个日期的差值
先计算每个日期是当年的第几天,再计算两个年份之间的总天数,最终得到两个日期的差值(包含起始日期,故加 1)。
代码实现:
#include <iostream>
using namespace std;
// 复用GetDay函数,获取指定年月的天数
int GetDay(int year, int month) {
static int MonthDay[13] = { -1,31,28,31,30,31,30,31,31,30,31,30,31 };
if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))) {
return 29;
}
return MonthDay[month];
}
// 复用Jisuan函数,计算日期是当年的第几天
int Jisuan(int year, int month, int day) {
int sum = 0;
for (int i = 1; i < month; i++) {
sum += GetDay(year, i);
}
return sum + day;
}
int main() {
int year1, year2, month1, month2, day1, day2;
int sum = 0;
// 读取两个日期(格式:YYYYMMDD YYYYMMDD)
while (scanf("%4d%2d%2d %4d%2d%2d", &year1, &month1, &day1, &year2, &month2, &day2) != EOF) {
int sum1 = Jisuan(year1, month1, day1); // 日期1的当年天数
int sum2 = Jisuan(year2, month2, day2); // 日期2的当年天数
// 计算年份差之间的总天数(假设year1 <= year2)
if (year1 < year2) {
for (int i = year1; i < year2; i++) {
// 闰年366天,平年365天
if ((i % 4 == 0 && i % 100 != 0) || (i % 400 == 0)) {
sum += 366;
} else {
sum += 365;
}
}
}
// 总差值 = 年份差天数 + (日期2当年天数 - 日期1当年天数) + 1(包含起始日)
sum += (sum2 - sum1);
cout << sum + 1 << endl;
sum = 0; // 重置sum,准备下一次计算
}
return 0;
}
九、总结
本文从构造函数的初始化列表切入,系统讲解了 C++ 类与对象的进阶知识,包括类型转换、static 成员、友元、内部类、匿名对象和编译器优化,并通过日期工具案例巩固了知识点。核心要点总结如下:
- 初始化列表是特殊成员变量(引用、const、无默认构造类)的唯一初始化方式,初始化顺序取决于成员声明顺序。
- 类型转换通过构造函数实现,
explicit
可禁止隐式转换,避免意外逻辑。 - static 成员属于类,需类外初始化,无 this 指针,适合实现共享资源(如计数器)。
- 友元突破封装,但增加耦合度,仅在必要时使用(如跨类访问私有成员)。
- 内部类是独立类,默认是外部类的友元,适合实现紧密关联的类逻辑。
- 匿名对象生命周期仅一行,适合临时使用,避免不必要的对象命名。
- 编译器优化可减少对象拷贝,提升效率,调试时可关闭优化查看完整过程。
掌握这些进阶知识,能帮助你更灵活地设计类结构,写出更高效、更符合 C++ 设计思想的代码。后续可通过更多实战案例,进一步加深对这些知识点的理解和应用。