类和对象(下)

目录

初始化列表

static成员

友元

内部类

匿名对象

隐式类型转换

拷贝对象时的一些编译器优化

日期类的实现和测试


初始化列表

在创建对象时,编译器通过调用构造函数,在构造函数体内给对象中的成员变量合适的初始值

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;
};

初始化列表的特点

  1. 调用构造函数时先走初始化列表进行初始化,后走函数体进行赋值等操作

  2. 拷贝构造函数属于特殊的构造函数,也可以使用初始化列表进行初始化

  3. 成员变量实际初始化的顺序取决于声明顺序,与在初始化列表中出现的次序无关,因此最好让初始化列表的顺序与成员变量的声明顺序一致,避免依赖未初始化的成员

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. 内置类型的成员变量如果既没有在初始化列表中进行初始化,也没有在函数体内赋值,若声明时给了缺省值,则使用该缺省值,若声明时没给缺省值,则是随机值

初始化列表的应用场景

类中包含以下成员变量,必须使用初始化列表进行初始化:

**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成员变量

  1. 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;
}
  1. 因为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.hA.cppB.cpp同时包含时,s_count = 0会被编译器分别编译到A.oB.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;
}
  1. 由于 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 成员函数

  1. static 成员函数 和 普通成员函数一样,都属于类,而非某个具体对象,存储也是在代码区 (仅一份拷贝),区别是static 成员函数没有隐含的 this 指针

因为 this 指针的本质作用是区分哪个对象在调用自己,毕竟所有对象共享同一套函数代码,必须通过 this 指向当前对象,才能访问该对象独有的成员变量 / 普通成员函数,而静态成员函数的功能定位是 "类级工具":通常用于处理和类相关的全局逻辑(比如统计对象创建个数、提供类级别的工具方法),而非处理某个对象的特有数据,因此不需要this指针!

  1. 可以直接访问类的 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;
};
  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;
}
  1. 非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
}
  1. 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;
}
  1. 内部类天生就是外部类的友元类,可以直接访问外部类的任何私有成员
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(返回值优化)和 "临时对象拷贝省略"

  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;
};

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;
}
  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()
}
  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()
{
	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;
}
相关推荐
Q741_1473 小时前
C++ 面试高频考点 链表 迭代 递归 力扣 25. K 个一组翻转链表 每日一题 题解
c++·算法·链表·面试·递归·迭代
syker3 小时前
手搓UEFI.h
c++
LIZhang20163 小时前
基于ffmpeg8.0录制mp4文件
开发语言·c++
_OP_CHEN3 小时前
C++进阶:(九)深度剖析unordered_map 与 unordered_set容器
开发语言·c++·stl容器·哈希表·哈希桶·unordered_map·unordered_set
freedom_1024_4 小时前
LRU缓存淘汰算法详解与C++实现
c++·算法·缓存
无敌最俊朗@4 小时前
C++-Qt-音视频-基础问题01
开发语言·c++
折戟不必沉沙4 小时前
C++四种类型转换cast,其在参数传递时的作用
c++
kyle~4 小时前
C++---万能指针 void* (不绑定具体数据类型,能指向任意类型的内存地址)
开发语言·c++
誰能久伴不乏4 小时前
Linux 进程通信与同步机制:共享内存、内存映射、文件锁与信号量的深度解析
linux·服务器·c++