目录
初始化列表
在创建对象时,编译器通过调用构造函数,在构造函数体内给对象中的成员变量合适的初始值
cpp
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量 的初始化,构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始 化一次,而构造函数体内可以多次赋值。
初始化列表是构造函数的特殊语法,用于在对象创建时直接初始化成员变量。其格式为:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟 一个放在括号中的初始值或表达式。
cpp
class Date
{
public:
Date(int year, int month, int day)
//初始化列表
:_year(year)
,_month(month)
,_day(day)
{} //函数体
private:
int _year;
int _month;
int _day;
};
初始化列表的特点
-
调用构造函数时先走初始化列表进行初始化,后走函数体进行赋值等操作
-
拷贝构造函数属于特殊的构造函数,也可以使用初始化列表进行初始化
-
成员变量实际初始化的顺序取决于声明顺序,与在初始化列表中出现的次序无关,因此最好让初始化列表的顺序与成员变量的声明顺序一致,避免依赖未初始化的成员
cpp
#include<iostream>
using namespace std;
class WrongOrder
{
private:
int a;
int b;
public:
// 初始化列表顺序:b 先初始化,再 a
// 但声明顺序:a 先声明,再 b → 实际初始化顺序:a 先,b 后
WrongOrder(int x)
:b(x)
,a(b)
{}
void show()
{
cout << "a:" << a << ", b:" << b << endl; // 预期 a=5, b=5;实际 a 是随机值,b=5
}
};
int main()
{
WrongOrder w(5);
w.show(); // 输出:a:随机值, b:5(因为 a 先初始化时,b 还未初始化)
return 0;
}
- 内置类型的成员变量如果既没有在初始化列表中进行初始化,也没有在函数体内赋值,若声明时给了缺省值,则使用该缺省值,若声明时没给缺省值,则是随机值
初始化列表的应用场景
类中包含以下成员变量,必须使用初始化列表进行初始化:
**1. 引用成员变量,**引用必须绑定一个有效的对象(不能悬空),且绑定后不能更改。因此必须在初始化列表中完成绑定
cpp
class A
{
private:
int& ref; // 引用成员
public:
// 正确:绑定到参数x
A(int& x)
:ref(x)
{}
// 错误:引用不能在构造函数体中赋值绑定
//A(int x)
//{
// ref = x;
//}
};
**2. const成员变量,**const 变量一旦声明必须初始化,且不能被修改,否则是无效常量,因为你不给初始值,后续还不能修改,那创建这个变量就没有意义了
cpp
class A
{
private:
const int num;
public:
// 正确:初始化列表初始化 const 成员
A(int n) : num(n) {}
// 错误:构造函数体中赋值给 const 成员
// A(int n) { num = n; }
};
但是只有下面这段代码时,并没有报错呀,因为这种写法只是 "类声明",只要不创建 A 的对象,编译器就不会检查 const 成员是否初始化 ------ 只有当你试图实例化(创建对象)时,才会因为 const 成员未初始化而报错
cpp
//没有报错
class A
{
private:
const int num;
};
cpp
class A
{
private:
const int num;
};
int main()
{
A a; //err
return 0;
}
3. 自定义类型成员且该成员没有默认构造函数时
cpp
class Date {
private:
int year, month, day;
public:
// 带参构造函数(默认构造被屏蔽)
Date(int y, int m, int d)
:year(y)
,month(m),
day(d)
{}
};
class Student {
private:
Date birthday; // 自定义类型成员(无默认构造)
public:
// 正确:初始化列表调用 Date 的带参构造
//Student(int y, int m, int d) : birthday(y, m, d) {}
// 错误:birthday 无法默认构造,编译失败
Student(int y, int m, int d) { birthday = Date(y, m, d); }
};
注:当 Student 对象被创建时,进入构造函数体之前,C++ 会先自动初始化所有成员变量,对于自定义类型成员 birthday,会调用它的默认构造,而它没有提供默认构造,因此直接报错!至于函数体里面的意思是,显式调用Date的带参构造函数生成临时对象然后赋值给 birthday 对象!
初始化列表的优势
对于自定义类型成员,若使用构造函数体赋值,会经历两步:成员变量先调用默认构造函数 初始化;构造函数体中调用赋值运算符重载函数修改值
初始化列表直接调用成员的带参构造函数,一步完成初始化,避免了默认构造和赋值的开销
因此对于有默认构造函数的自定义类型成员,若需指定初始值,仍建议用初始化列表(效率更高)
而内置类型(int、double 等)在初始化列表和构造函数体中赋值效率差异极小, 不用纠结!
cpp
#include<iostream>
using namespace std;
class MyString
{
public:
MyString() { cout << "默认构造" << endl; } // 默认构造
MyString(const string& s) { cout << "带参构造" << endl; } // 带参构造
MyString& operator=(const string& s) { cout << "赋值运算符" << endl; return *this; } // 赋值
};
class Test
{
private:
MyString str;
public:
// 方式1:构造函数体赋值(低效)
Test(const string& s)
{
str = s; // 先默认构造 str,再赋值
}
// 方式2:初始化列表(高效)
Test(const string& s) : str(s)
{
// 直接调用带参构造,一步完成
}
};
int main() {
cout << "方式1:" << endl;
Test t1("hello"); // 输出:默认构造 → 赋值运算符
cout << "方式2:" << endl;
Test t2("world"); // 输出:带参构造
return 0;
}
说明:C++11允许内置成员变量给缺省值,如果初始化列表中没有显示给该成员变量传参,就会使用缺省值作为参数初始化该成员变量,本质还是走的初始化!
static成员
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用
static修饰的成员函数,称之为静态成员函数。
static成员变量
- static成员变量属于类本身,不在对象的内存空间中(对象只存储非 static 成员变量),而是存储在全局数据区 (静态存储区),程序启动时分配内存,程序结束时释放,生命周期是 "整个程序运行期间",必须在程序启动时就分配好唯一的一块内存
验证 static 成员变量不存在于对象里:
cpp
#include<iostream>
using namespace std;
class A
{
public:
int a;
static int b;
};
int main()
{
cout << sizeof(A) << endl; //4
return 0;
}
- 因为static成员变量属于类本身,因此所有类对象共享同一个 static 成员变量,修改一个对象的 static 变量会影响所有其他对象
3. 静态成员变量不能在类内初始化,必须在类外完成
cpp
class A
{
public:
A(int x, int y, int z)
:_x(x)
,_y(y)
,_z(z) //err
{}
private:
int _x;
int _y;
static int _z;
};
class B
{
private:
int _x;
int _y;
static int _z = 1; //err, 给缺省值本质也是在初始化列表中初始化
};
原因1:静态成员变量(如s_count):它属于类本身 ,不随对象创建 / 销毁,生命周期是 "整个程序运行期间",必须在程序启动时就分配好唯一的一块内存。而类声明不负责分配内存,所以无法在类内完成 "初始化 + 内存分配" 的绑定
原因2:头文件(如Test.h)通常会被多个 .cpp 文件 include,如果允许静态成员变量在类内初始化,会触发严重的"重复定义"问题:
cpp
// 假设允许类内初始化(实际编译报错)
class Test {
static int s_count = 0; // 错误假设:类内初始化
};
当Test.h被A.cpp和B.cpp同时包含时,s_count = 0会被编译器分别编译到A.o和B.o中,链接阶段,链接器会发现两个Test::s_count的定义(都有内存分配和初始化),违反 "一个变量只能定义一次" 的 C++ 规则,直接报错误。
正确的做法是类内声明,类外进行初始化,注意,static成员类内声明,类外定义是一个完整的整体,而private访问限定符限定的类外"访问"私有成员变量,这里是定义,不是访问,不会触发private 机制!
cpp
// Test.h(头文件:仅声明,无初始化)
class Test {
static int s_count;
};
// Test.cpp(源文件:仅定义一次,分配内存+初始化)
int Test::s_count = 0; //必须要指明类域
因为.cpp文件不会被重复包含,Test::s_count的定义只出现一次,链接时不会冲突。
特例:const static 修饰的整形成员变量(int/char/long等)可以类内初始化
cpp
class Test
{
static const int s_max = 100; // 允许(const static 整型)
static const char s_flag = 'A'; // 允许(const static 字符型)
};
因为这类变量是 编译期常量 (值在编译时就确定,不会变),编译器可以直接把它当作 "常量字面量" 嵌入代码(比如int arr[Test::s_max],编译时就知道数组大小是 100),不需要依赖 "运行时内存分配"。
但如果是非整形的 const static 变量 ,依然不能类内初始化,因为double 这类类型的"常量性"依赖运行时(比如浮点数精度可能受编译选项影响),不能当作"编译期字面量",必须在类外定义
• 验证 static 成员变量被所有对象共享,修改一个对象的 static 变量会影响所有其他对象
cpp
#include <iostream>
using namespace std;
class A
{
public:
static int _a;
};
int A::_a = 10;
int main()
{
A a1;
A a2;
a1._a = 20;
cout << a2._a << endl;
return 0;
}
- 由于 static 成员变量不属于具体的对象,因此在类外面访问 static 成员变量可以不用对象访问(也可以用),可以直接 类 :: 变量
cpp
#include <iostream>
using namespace std;
class A
{
public:
static int _z;
};
int A::_z = 2;
int main()
{
A a;
cout << a._z << endl;
cout << A::_z << endl;
return 0;
}
static 成员函数
- static 成员函数 和 普通成员函数一样,都属于类,而非某个具体对象,存储也是在代码区 (仅一份拷贝),区别是static 成员函数没有隐含的 this 指针
因为 this 指针的本质作用是区分哪个对象在调用自己,毕竟所有对象共享同一套函数代码,必须通过 this 指向当前对象,才能访问该对象独有的成员变量 / 普通成员函数,而静态成员函数的功能定位是 "类级工具":通常用于处理和类相关的全局逻辑(比如统计对象创建个数、提供类级别的工具方法),而非处理某个对象的特有数据,因此不需要this指针!
- 可以直接访问类的 static 成员变量和 static 成员函数,不能直接访问非 static 成员变量和非 static成员函数(因为需要 this 指针指向具体对象)
cpp
class A
{
public:
void func1() {}
static void func2() {}
static void Print()
{
// cout << _x << _y << endl; //err, 不能访问非静态成员变量
cout << _z << endl; //可以访问静态成员变量
// func1(); //不能访问非静态成员函数
func2(); //可以访问静态成员函数
}
private:
int _x = 1;
int _y = 1;
static int _z;
};
int A::_z = 2;
如果非要让 static 成员函数访问 非static成员,那么就要显式传递 类类型的对象给函数形参
cpp
class A
{
public:
void func1() {}
static void Print(A& a)
{
cout << a._x << " " << a._y << endl;
a.func1();
}
private:
int _x = 1;
int _y = 1;
};
- 类外访问static成员函数,不是必须通过对象调用(也可以),只要突破类域就可以直接访问
cpp
#include<iostream>
using namespace std;
class A
{
public:
static void Print()
{
cout << "static void Print()" << endl;
}
};
int main()
{
A::Print(); //只要突破类域,就可以直接访问静态成员函数
A a;
a.Print(); //当然也可以用对象去调用
return 0;
}
- 非static成员函数含有this指针,可以直接调用static成员函数,而static成员函数中没有this指针,不能直接调用非static成员函数
cpp
#include<iostream>
using namespace std;
class A
{
public:
//非static成员函数可以直接调用static成员函数
void func()
{
Print();
}
static void Print()
{
cout << "static void Print()" << endl;
}
};
int main()
{
A a;
a.func(); //static void Print()
return 0;
}
static成员的应用
1.实现一个类,计算程序中创建出了多少个对象以及正在使用多少个对象
cpp
#include<iostream>
using namespace std;
class A
{
public:
A()
{
++_n;
++_m;
};
A(const A& a)
{
++_n;
++_m;
};
~A()
{
--_m;
}
void Print()
{
cout << "累计创建对象: " << _n << " " << "正在使用对象: " << _m << endl;
}
private:
static int _n;
static int _m;
};
//静态成员变量初始化
int A::_n = 0; //累计创建了多少个对象
int A::_m = 0; //正在使用的有多少个对象
A Func(A aa)
{
return aa;
}
int main()
{
A aa1;
aa1.Print(); //1 1
A aa2;
Func(aa2);
aa2.Print(); //4 2
}
- OJ题目:求1+2+3+...+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)
cpp
class Sum {
public:
Sum() {
_ret += _i;
_i++;
}
static int& GetRet() {
return _ret;
}
private:
static int _i;
static int _ret;
};
int Sum::_i = 1;
int Sum::_ret = 0;
class Solution {
public:
int Sum_Solution(int n) {
Sum a[n];
return Sum::GetRet();
}
};
友元
在 C++ 中,友元(Friend) 是一种打破类封装性的机制,允许外部函数、其他类或其他类的成员函数直接访问当前类的 私有(private) 和 保护(protected) 成员(变量和函数)
外部友元函数
外部函数通过 friend 声明成为当前类的友元函数,可直接访问类的私有 / 保护成员,友元函数不属于该类的成员函数,其声明可放在类的 public、private、或 protected 区域,效果完全相同
cpp
#include <iostream>
using namespace std;
class A
{
friend void func();
private:
int _a = 10;
};
void func()
{
A a;
cout << a._a << endl;
}
int main()
{
func(); //10
return 0;
}
对于内置类型,我们可以直接使用 cin / cout 进行输入输出,非常方便,如果要对自定义类型对象也使用 cin / cout,我们需要对 >> 和 << 进行运算符重载!比如有日期类,假如我们将 >> 和 << 重载成类的成员函数:
cpp
class Date
{
public:
void operator>>(istream& in)
{
in >> _year >> _month >> _day;
}
void operator<<(ostream& out)
{
out << _year << " " << _month << " " << _day;
}
private:
int _year;
int _month;
int _day;
};
因为类的非static成员函数第一个参数默认是this指针,因此我们只能把 输入流对象 和 输出流对象作为第二个参数了,这导致我们在外部调用的时候,使用起来很别扭,只能如下写了:
cpp
int main()
{
Date d1;
d1 >> cin;
d1 << cout;
return 0;
}
也是完全正确的,但不符合我们的使用习惯,因此我们一般将 >> 和 << 都重载成全局函数,但重载成全局函数又访问不了 类的私有成员变量了,于是我们将这两个函数声明为日期类的友元函数
并且为了支持 << 和 >> 的连续使用,我们将两个函数的返回值进行修改!
cpp
class Date
{
friend istream& operator>>(istream& in, Date& d);
friend ostream& operator<<(ostream& in, const Date& d);
private:
int _year;
int _month;
int _day;
};
//出了函数, in对象还在, 可以返回引用
istream& operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
//出了函数, out对象还在, 可以返回引用
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << " " << d._month << " " << d._day;
return out;
}

说明:
● 友元函数不能用const修饰,因为不是类的成员函数,没有this指针
● 一个函数可以是多个类的友元函数
● 友元函数的调用与普通函数的调用原理相同
友元类
整个类 B 被声明为类 A 的友元,则类 B 的所有成员函数都能直接访问类 A 的私有 / 保护成员。
cpp
#include <iostream>
using namespace std;
class A
{
friend class B;
public:
A(int a1, int a2)
:_a1(a1)
,_a2(a2)
{}
private:
int _a1;
int _a2;
};
class B
{
public:
void func(A& a)
{
//B类是A类的友元, 可以直接访问呢A类的所有私有成员
cout << a._a1 << " " << a._a2 << endl;
}
};
int main()
{
A a(1, 2);
B b;
b.func(a); //1 2
return 0;
}
说明:
● 友元关系是单向的,B是A的友元,不代表A是B的友元
● 友元关系不能传递,如果C是B的友元, B是A的友元,不代表C是A的友元
● 友元关系不能继承,子类不会继承父类的友元关系(除非子类自己声明)
其他类的成员函数作友元函数
仅允许其他类的某个特定成员函数成为当前类的友元,比友元类更精准(避免整个类被授权)
要注意各种前向声明,否则可能会报各种编译报错
cpp
#include <iostream>
using namespace std;
class A; // 前向声明:告诉编译器A是一个类
class B
{
public:
// 仅声明func成员函数(不实现),此时无需知道A的具体成员
void func(A& a);
void f(A& a);
};
class A
{
friend void B::func(A& a); // 声明B::func为友元(此时B已声明func,友元生效)
public:
A(int a1, int a2)
: _a1(a1)
, _a2(a2)
{}
private:
int _a1;
int _a2;
};
// 实现B::func(此时A已完整定义,可访问其私有成员)
void B::func(A& a)
{
cout << a._a1 << " " << a._a2 << endl;
}
void B::f(A& a)
{
//cout << a._a1 << " " << a._a2 << endl; //err, 不是A类的友元函数, 无法访问
}
int main()
{
A a(1, 2);
B b;
b.func(a); // 输出:1 2(正常运行)
return 0;
}
友元的优缺点
优点:
● 提高代码灵活性:在不破坏整体封装的前提下,解决特定场景的访问需求。
● 简化代码:避免为了访问私有成员而编写大量冗余的公有接口(如 getXXX(),setXXX())。
缺点:
● 破坏封装性:友元直接访问私有成员,违反了 "数据隐藏" 的面向对象原则,增加了代码耦合度。
● 降低可维护性:若友元过多,类的私有成员被多个外部实体访问,后续修改私有成员时需同步修改所有友元,维护成本高。
内部类
如果A类定义在B类的内部(可在public / protected / private 下),B就叫做A类的内部类
内部类的特点
1.内部类是一个独立的类, 它不属于外部类,外部类对内部类没有任何优越的访问权限,但受外部类作用域限制,其核心特性是封装性和作用域隔离
cpp
#include<iostream>
using namespace std;
class A
{
public:
void funcA()
{
B b;
//cout << b._b << endl; //err, 不可访问, 证明外部类对内部类没有任何优越的访问权限
}
class B
{
public:
static int _bb;
private:
int _b;
};
private:
int _a;
};
int A::B::_bb = 10;
int main()
{
cout << sizeof(A) << endl; //4, 证明内部类是完全独立的类
cout << A::B::_bb << endl; //10, 内部类受外部类作用域限制
return 0;
}
- 内部类天生就是外部类的友元类,可以直接访问外部类的任何私有成员
cpp
#include<iostream>
using namespace std;
class A
{
public:
class B
{
public:
void Print(A& a)
{
cout << a._a << endl;
a.funcA();
}
private:
int _b;
};
private:
void funcA() { cout << "void funcA()" << endl; };
int _a = 1;
};
int main()
{
A a;
A::B b;
b.Print(a); //打印 1 以及 void funcA()
}
使用内部类的使用简化OJ题目---1+2+3+···n
cpp
class Solution {
class Sum {
public:
Sum() {
_ret += _i;
_i++;
}
};
public:
int Sum_Solution(int n) {
Sum a[n]; //g++支持变长数组
return _ret;;
}
private:
static int _i;
static int _ret;
};
int Solution::_i = 1;
int Solution::_ret = 0;
匿名对象
匿名对象是指没有显式命名的临时对象,它仅在创建时所在的行(或表达式)中存在,生命周期极短(通常在当前语句结束后销毁),创建对象时不指定变量名,直接通过构造函数初始化
cpp
#include<iostream>
using namespace std;
class A
{
public:
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
int main()
{
A(); // 匿名对象
cout << "hello world" << endl;
//打印结果:
// ~A()
// hello world
return 0;
}
匿名对象的用途
主要用于单次成员函数调用、临时参数传递、函数返回值
假设有如下类:
cpp
class Person {
public:
Person(string name, int age) {
this->_name = name;
this->_age = age;
cout << "Person构造:" << name << endl;
}
void showInfo() {
cout << "姓名:" << _name << ",年龄:" << _age << endl;
}
private:
string _name;
int _age;
};
● 当仅需调用对象的某个成员函数一次,无需重复使用该对象时,匿名对象可简化代码。
cpp
#include <iostream>
using namespace std;
int main() {
// 匿名对象:直接调用showInfo(),无需定义变量
Person("张三", 20).showInfo();
// 对比:命名对象
Person p("李四", 25);
p.showInfo();
return 0;
}
● 当函数参数为 "对象类型"(非指针 / 引用)时,可直接传递匿名对象,避免临时变量定义
cpp
// 函数:接收Person对象作为参数
void printPerson(Person p) {
p.showInfo();
}
int main() {
// 匿名对象作为参数直接传递
printPerson(Person("王五", 30)); // 匿名对象被拷贝到函数参数p,函数结束后p析构
return 0;
}
● 函数可直接返回匿名对象,编译器会进行优化,避免不必要的拷贝
cpp
#include <iostream>
using namespace std;
Person createPerson(string name, int age) {
// 返回匿名对象,编译器会优化为直接构造到接收变量
return Person(name, age);
}
int main() {
// 接收函数返回的匿名对象(优化后无额外拷贝)
Person p = createPerson("赵六", 35);
p.showInfo();
return 0;
}
匿名对象注意事项
● 不要保存匿名对象的 指针 / 引用,因为匿名对象销毁后,指针 / 引用会变成 野指针(非法访问可能会崩溃) / 悬垂引用(编译直接报错)
cpp
Person* p = &Person("张三", 20); // 错误!匿名对象销毁后,p指向无效内存
p->showInfo(); // 未定义行为(内存访问错误)
● 无参构造的匿名对象:Person( ) 是匿名对象,而 Person p() 是函数声明,无参,返回值类型为Person类
cpp
Person p1; // 命名对象(无参构造)
Person(); // 匿名对象(无参构造)
// Person p2(); // 错误!不是对象定义,是函数声明
● const引用绑定匿名对象,会将匿名对象的生命周期延长至引用的生命周期结束

● 匿名对象仅适用于 "临时使用一次" 的场景,若需多次访问对象,应定义命名对象(否则会重复创建 / 销毁,影响效率)
隐式类型转换
单参数构造函数的隐式类型转换
cpp
class A
{
public:
A(int i)
:_a(i)
{
cout << "A(int i)" << endl;
}
private:
int _a;
};
正常创建对象是如下方式创建的:
cpp
A aa1(1);
而下面写法编译器也是支持的,下面这种写法的本质是编译器使用整形2调用构造函数生成一个临时对象,然后再用这个临时对象拷贝构造aa2,这个过程称为单参数构造函数的隐式类型转换,也就是将 构造函数参数类型 转化为 构造函数所属类的类型
cpp
A aa2 = 2; //本质是 A aa2 = A(2);
如何证明是生成了临时对象呢?
cpp
// A& ref = 2;//×, 普通引用不能绑定临时对象
const A &ref = 2; //√, const引用可以绑定临时对象
多参数构造函数的隐式类型转换
cpp
#include <iostream>
using namespace std;
class B
{
public:
B(int b1, int b2)
:_b1(b1)
, _b2(b2)
{
cout << "B(int b1, int b2)" << endl;
}
private:
int _b1;
int _b2;
};
int main()
{
//C++11 支持多参数的隐式类型转换
B bb1(1, 1);
B bb2 = { 2, 2 };
const B& ref2 = { 3,3 };
}
隐式类型转化的用途
主要用于简化代码,编译器会自动帮助我们进行类型转换,不需要我们显式去写!
cpp
#include <iostream>
using namespace std;
class A
{
public:
A(int x = 1)
{
_x = x;
}
private:
int _x;
};
typedef A DataType;
//栈中存储的是A类型的对象
class Stack
{
public:
Stack(int capacity = 10)
{
_top = 0;
_capacity = capacity;
_a = (DataType*)malloc(sizeof(DataType) * _capacity);
}
void Push(DataType x)
{
_a[_top] = x;
_top++;
}
private:
DataType* _a;
int _top;
int _capacity;
};
int main()
{
Stack s;
//正常入栈写法
A a1(1);
s.Push(a1);
A a2(2);
s.Push(a2);
A a3(3);
s.Push(a3);
//隐式类型转化简化代码
s.Push(1);
s.Push(2);
s.Push(3);
}
简单场景可利用隐式转换简化代码,但需要严格类型检查时,用 explicit 关键字修饰构造函数可以禁止发生构造函数的隐式类型转化
cpp
#include <iostream>
using namespace std;
class A
{
public:
explicit A(int a)
:_a(a)
{
cout << "explicit A(int a)" << endl;
}
private:
int _a;
};
int main()
{
A a1(1);
// A a2 = 2; //(×)
// const A& ref3 = 3; //(×)
}
拷贝对象时的一些编译器优化
为了提高程序运行的效率,现代编译器基本默认都会开启一些优化策略,主要是 RVO(返回值优化)和 "临时对象拷贝省略"
- 在一个表达式中,构造函数与拷贝构造函数紧接执行,优化成直接构造
cpp
#include <iostream>
using namespace std;
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A()" << endl;
}
A(const A& a)
:_a(a._a)
{
cout << "const A& a" << endl;
}
private:
int _a = 0;
};
void f1(A aa)
{}
int main()
{
//先调用构造函数创建aa1对象,再调用拷贝构造函数将aa1对象拷贝给aa对象
A aa1(1);
f1(aa1);
cout << "-----------------------------" << endl;
//本来:先调用构造函数创建匿名对象,再调用拷贝构造将匿名对象拷贝给aa对象
//实际:直接调用构造函数传1创建aa对象
f1(A(1));
cout << "-----------------------------" << endl;
//本来:先调用构造函数创建匿名对象,再调用拷贝构造将匿名对象拷贝给aa3对象
//实际:直接调用构造函数传1创建aa3对象
A aa3 = A(1);
cout << "-----------------------------" << endl;
//本来:先进行构造函数的单参数的隐式类型转换创建临时对象,再调用拷贝构造函数将临时对象拷贝给aa对象
//实际:直接调用构造函数传1创建aa对象
f1(1);
cout << "-----------------------------" << endl;
//本来:先进行构造函数的单参数的隐式类型转换创建临时对象,再调用拷贝构造函数将临时对象拷贝给aa2对象
//实际:直接调用构造函数传1创建aa2对象
A aa2 = 1;
}
- 连续的两次拷贝构造优化成一次拷贝构造
cpp
#include <iostream>
using namespace std;
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A()" << endl;
}
A(const A& a)
:_a(a._a)
{
cout << "const A& a" << endl;
}
private:
int _a = 0;
};
A f2()
{
A aa; //调用构造函数
return aa; //返回局部对象时,会生成该对象的临时拷贝对象
}
int main()
{
//本来应该是两次拷贝构造,但经过编译器的优化, aa直接拷贝构造ret1, 省去了拷贝构造临时对象的步骤
A ret1 = f2(); //A()
}
- 连续的 一次构造 + 两次拷贝 优化成直接构造
cpp
#include <iostream>
using namespace std;
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A()" << endl;
}
A(const A& a)
:_a(a._a)
{
cout << "const A& a" << endl;
}
private:
int _a = 0;
};
A f2()
{
return A(1); //本来应该先调用构造函数传1创建匿名对象, 再拷贝构造给临时对象, 再将临时对象拷贝给ret1
}
int main()
{
//经过编译器优化,直接用1构造对象A
A ret1 = f2(); //A()
}
cpp
#include <iostream>
using namespace std;
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A()" << endl;
}
A(const A& a)
:_a(a._a)
{
cout << "const A& a" << endl;
}
private:
int _a = 0;
};
A f2()
{
//本来先调用构造函数,将2隐式类型转化成类类型,创建出临时对象1,再调用拷贝构造函数将临时对象1拷贝构造给临时对象2,再将临时对象2拷贝构造给ret2
return 2;
}
int main()
{
//编译器优化后,直接用2构造对象ret2
A ret2 = f2(); //A()
}
日期类的实现和测试
有了上面知识的储备,我们现在可以完整的实现一个日期类,接口如下:
cpp
class Date
{
//友元声明
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
public:
//获取某年某月的天数
int GetMonthDay(int year, int month) const;
//默认构造函数
Date(int year = 1, int month = 1, int day = 1);
//打印函数
void Print() const;
//运算符重载函数
bool operator>(const Date& d) const;
bool operator==(const Date& d) const;
bool operator>=(const Date& d) const;
bool operator!=(const Date& d) const;
bool operator<(const Date& d) const;
bool operator<=(const Date& d) const;
Date& operator+=(int day);
Date operator+(int day) const;
Date& operator-=(int day);
Date operator-(int day) const;
Date& operator=(const Date& d);
Date& operator++();
Date operator++(int);
Date& operator--();
Date operator--(int);
int operator-(const Date& d) const;
Date* operator&();
const Date* operator&()const;
private:
int _year;
int _month;
int _day;
};
获取某年某月的天数
cpp
int Date::GetMonthDay(int year, int month) const
{
//const修饰,数组不会被更改; static修饰,数组只需要创建一次
const static int MonthArray[13] = { 0, 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 MonthArray[month];
}
构造函数
cpp
Date::Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
//检查日期是否合法
if (month < 1 || month > 12 || day > GetMonthDay(year, month))
{
cout << "日期非法" << endl;
}
}
打印函数
cpp
void Date::Print() const
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
大于运算符重载
先比较年,年大则大;年相等,再比较月,月大则大;月相等,再比较日,日大则大
cpp
bool Date::operator>(const Date& d) const
{
if (_year > d._year)
{
return true;
}
else if (_year == d._year && _month > d._month)
{
return true;
}
else if (_year == d._year && _month == d._month && _day > d._day)
{
return true;
}
else
{
return false;
}
}
等于运算符重载
cpp
bool Date::operator==(const Date& d) const
{
if (_year == d._year && _month == d._month && _day == d._day)
{
return true;
}
else
{
return false;
}
}
实现了大于运算符重载和等于运算符重载,其余的比较运算符直接复用这两个函数即可
cpp
bool Date::operator>=(const Date& d) const
{
return *this > d || *this == d;
}
bool Date::operator!=(const Date& d) const
{
return !(*this == d);
}
bool Date::operator<(const Date& d) const
{
return !(*this >= d);
}
bool Date::operator<=(const Date& d) const
{
return !(*this > d);
}
+= 运算符重载
● 直接在当前日期对象的天数上加上day,然后判断是否超出了当月的天数,如果超出了,那就-=当月的天数,月份++,紧接着判断月份是否超过12月,超过12月,年份++,月份归1,循环往复,一直到天数小于等于当月的天数
● 出了作用域,对象还在,因此我们返回对象的引用
● 天数可能小于0,直接复用-=运算符重载函数
● += 会改变对象本身,因此不能用const修饰函数
cpp
Date& Date::operator+=(int day)
{
if (day < 0)
{
return *this -= (-day);
}
_day += day;
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
++_month;
if (_month == 13)
{
++_year;
_month = 1;
}
}
return *this;
}
+ 运算符重载
● + 不会改变对象本身,因此使用const修饰函数
● + 需要返回的是 + 完之后的值,因此我们需要在函数内部创建临时对象,完成操作,而临时对象 出了作用域就不在了,因此我们不能返回对象的引用
● 先用当前对象拷贝构造tmp对象,然后tmp对象调用+=运算符重载函数,最后将tmp返回
cpp
Date Date::operator+(int day) const
{
Date tmp(*this); //拷贝一份放到类tmp中, 等价于 Date tmp = *this;
tmp += day;
return tmp;
}
拓展: +运算符可以复用+=,其实+=也可以复用+,但更推荐前者,因为前者实现进行了两次拷贝构造,分别发生在拷贝构造tmp和返回tmp时拷贝构造临时对象,而后者实现进行了两次拷贝构造+一次赋值重载,更加耗时
cpp
Date Date::operator+(int day) const
{
Date tmp(*this); //拷贝构造
tmp._day += day;
while (tmp._day > GetMonthDay(_year, _month))
{
tmp._day -= GetMonthDay(_year, _month);
++tmp._month;
if (tmp._month == 13)
{
++tmp._year;
tmp._month = 1;
}
}
return tmp; //拷贝构造
}
Date& Date::operator+=(int day)
{
*this = *this + day; //赋值重载
return *this;
}
-= 运算符重载
● 直接在当前日期对象的天数上减去day,然后判断天数是否小于等于0,如果是,那就月份--,紧接着判断月份是否减到了0,如果月份减到了0,年份--,月份归12,然后当前对象的天数加上当前月份的天数,循环往复,一直到天数大于0
● 出了作用域,对象还在,因此我们返回对象的引用
● 天数可能小于0,直接复用+=运算符重载函数
● -= 会改变对象本身,因此不能用const修饰函数
cpp
Date& Date::operator-=(int day)
{
if (day < 0)
{
return *this += (-day);
}
_day -= day;
while (_day <= 0)
{
_month--;
if (_month == 0)
{
_year--;
_month = 12;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
- 运算符重载
● - 不会改变对象本身,因此使用const修饰函数
● - 需要返回的是 - 完之后的值,因此我们需要在函数内部创建临时对象,完成操作,而临时对象 出了作用域就不在了,因此我们不能返回对象的引用
● 先用当前对象拷贝构造tmp对象,然后tmp对象调用-=运算符重载函数,最后将tmp返回
cpp
Date Date::operator-(int day) const
{
Date tmp(*this);
tmp -= day;
return tmp;
}
赋值运算符重载
cpp
Date& Date::operator=(const Date& d)
{
//避免自己给自己赋值
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
前置++/后置++/前置--/后置-- 运算符重载
由于前置++/后置++,参数都只有this指针,调用形式也是一样的,因此为了区分前置++和后置++,编译器进行了特殊处理,给后置++加一个int参数进行占位,与前置++构成函数重载,进行区分,前置--和后置--也是同样的道理
cpp
//前置++
Date& Date::operator++()
{
//返回++后的值
*this += 1;
return *this;
}
//后置++
Date Date::operator++(int)
{
//返回++前的值
Date tmp(*this);
*this += 1;
return tmp;
}
//前置--
Date& Date::operator--()
{
*this -= 1;
return *this;
}
//后置--
Date Date::operator--(int)
{
Date tmp(*this);
*this -= 1;
return tmp;
}
日期 - 日期 运算符重载
日期+日期是没有任何实际意义的,而日期-日期可以计算出两日期之间差了多少天
cpp
int Date::operator-(const Date& d) const
{
//假设日期A>日期B
Date max = *this;
Date min = d;
int flag = 1;
//如果小于调整即可
if (*this < d)
{
max = d;
min = *this;
flag = -1;
}
int n = 0;
while (min != max)
{
min++;
n++;
}
return n * flag;
}
取地址运算符重载和const取地址运算符重载
cpp
//取地址运算符重载函数
Date* Date::operator&()
{
return this;
//return nullptr; ---不想被取地址
}
//const取地址运算符重载函数
const Date* Date::operator&()const
{
return this;
}
流插入运算符重载
cpp
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out; //支持连续流插入
}
流提取运算符重载
cpp
istream& operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in; //支持连续流插提取
}
日期类的测试
cpp
#include "Date.h"
void TestDate1() //测试运算符重载函数
{
Date d1(2023, 7, 23);
Date d2(2023, 8, 1);
cout << (d1 > d2) << endl; //等价于 d1.operator>(d2);
cout << (d1 == d2) << endl; //等价于 d1.operator==(d2);
cout << (d1 >= d2) << endl; //等价于 d1.operator>=(d2);
cout << (d1 != d2) << endl; //等价于 d1.operator!=(d2);
cout << (d1 < d2) << endl; //等价于 d1.operator<(d2);
cout << (d1 <= d2) << endl; //等价于 d1.operator<=(d2);
}
void TestDate2() //测试 += 与 +
{
//+=
Date d1(2023, 7, 23);
Date ret1 = d1 += 150;
ret1.Print();
//+
Date d2(2023, 8, 1);
Date ret2 = d2 + 33; //等价于Date ret2(d7 + 33);
ret2.Print();
}
void TestDate3() //测试 -= 与 -
{
//-=
Date d1(2023, 7, 23);
Date ret1 = d1 -= 33;
ret1.Print();
//-
Date d2(2023, 1, 13);
Date ret2 = d2 - 23;
ret2.Print();
}
void TestDate4() // 测试 += -= + - 负数
{
Date d1(2023, 8, 1);
Date ret1 = d1 += -100;
ret1.Print();
Date d2(2023, 8, 1);
Date ret2 = d2 -= -100;
ret2.Print();
}
void TestDate5() //测试拷贝构造函数与赋值运算符重载函数
{
//拷贝构造和赋值重载是有区别的
//拷贝构造:一个已经存在的对象去初始化另一个要创建的对象
Date d3(2023, 7, 31);
Date d4(d3);
//赋值重载:两个已存在对象进行拷贝
Date d5(2023, 8, 1);
Date d6;
d6 = d5; //等价于d6.operator=(d5);
//连续赋值
d3 = d6 = d5; // d6 = d5 表达式的返回值是d6
d3.Print();
}
void TestDate6() //测试前置++,后置++,前置--,后置--
{
Date d1(2023, 7, 31);
Date ret1 = ++d1;
//可以显式调用 Date ret1 = d1.operator++();
d1.Print();
ret1.Print();
Date d2(2023, 7, 31);
Date ret2 = d2++;
//可以显式调用 Date ret2 = d2.operator++(int);
d2.Print();
ret2.Print();
Date d3(2023, 7, 31);
Date ret3 = --d3;
d3.Print();
ret3.Print();
Date d4(2023, 7, 31);
Date ret4 = d4--;
d4.Print();
ret4.Print();
}
void TestDate7() //测试日期-日期
{
Date d1(2023, 8, 1);
Date d2(2004, 3, 19);
cout << d1 - d2 << endl;
}
void TestDate8() //测试权限问题
{
const Date d1;
d1.Print(); //权限平移
Date d2;
d2.Print(); //权限缩小
}
void TestDate9() //测试取地址运算符重载函数
{
Date d1(2023, 8, 1);
cout << &d1 << endl; //编译器调用了取地址运算符重载
const Date d2(2023, 8, 2);
cout << &d2 << endl; //编译器调用了取地址运算符重载
}
void TestDate10() //测试流插入与流提取重载函数
{
//测试流插入
Date d1(2023, 8, 1);
cout << d1;
//测试流提取
Date d2;
Date d3;
cin >> d2 >> d3;
cout << d2 << d3;
}
int main()
{
TestDate1();
TestDate2();
TestDate3();
TestDate4();
TestDate5();
TestDate6();
TestDate7();
TestDate8();
TestDate9();
TestDate10();
return 0;
}