【C++】类和对象——Lesson2

Hi~!这里是奋斗的小羊,很荣幸您能阅读我的文章,诚请评论指点,欢迎欢迎 ~~
💥💥个人主页:奋斗的小羊
💥💥所属专栏:C++


🚀本系列文章为个人学习笔记,在这里撰写成文一为巩固知识,二为记录我的学习过程及理解。文笔、排版拙劣,望见谅。


目录

1、赋值运算符重载

1.1运算符重载

当运算符被用于类类型的对象时,C++允许我们通过运算符重载的形式指定新的定义。C++规定类类型对象使用运算符时,必须转换成调用对应运算符重载,若没有对应的运算符重载,则会编译报错。

  • 运算符重载是具有特殊名字的函数,它的名字是由operator和后面要定义的运算符共同构成,和其他函数一样,它也具有其返回类型和参数列表以及函数体
  • 重载运算符函数的参数个数和该运算符作用的运算对象数量一样多。一元运算符有一个参数,二元运算符有两个参数,二元运算符的左侧运算对象传给第一个参数,右侧运算对象传给第二个参数
cpp 复制代码
#include <iostream>
using namespace std;

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

	//类中成员变量都可以访问
	//int Get()
	//{
	//	return _year;
	//}

//private:
	int _year;
	int _month;
	int _day;
};

bool operator<(Date d1, Date d2)
{
	return d1._year < d2._year
		&& d1._month < d2._month
		&& d1._day < d2._day;
}

int main()
{
	Date x(2024, 7, 28);
	Date y(2004, 11, 7);
	operator<(x, y);//这样写和普通函数没什么区别
	//可以像下面这样调用,和上面一样
	x < y;
	return 0;
}
  • 如果一个重载运算符函数是成员函数 ,则它的第一个运算对象默认传给隐式的this指针,因此运算符重载作为成员函数时,参数比运算对象少一个
cpp 复制代码
#include <iostream>
using namespace std;

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

	//类中成员变量都可以访问
	//int Get()
	//{
	//	return _year;
	//}

	bool operator<(Date d2)
	{
		return _year < d2._year
			&& _month < d2._month
			&& _day < d2._day;
	}

//private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date x(2024, 7, 28);
	Date y(2004, 11, 7);

	//operator<(x, y);//这样写和普通函数没什么区别
	//可以像下面这样调用,和上面一样
	//x < y;

	//x传给this指针,y传给d2
	x.operator<(y);
	x < y;

	return 0;
}
  • 运算符重载后,其优先级和结合性与对应的内置类型运算符保持一致
  • 不能通过连接语法中没有的符号来创造新的操作符,比如operator@
  • .* :: sizeof ?: . 以上5个运算符不能重载
  • 重载操作符至少有一个类类型参数,不能通过运算符重载改变内置类型对象的含义,如:int operator+(int x, int y);
  • 一个类需要重载哪些运算符,是看哪些运算符重载后有意义,比如Date类重载operator-就有意义,但是重载operator+就没有意义
  • 重载++运算符时,有前置++和后置++,运算符重载函数名都是operator++,无法很好的区分。C++规定,后置++重载时,增加⼀个int形参,跟前置++构成函数重载,方便区分
  • 重载<<和>>时,需要重载为全局函数,因为重载为成员函数,this指针默认抢占了第一个形参位置,第⼀个形参位置是左侧运算对象,调⽤时就变成了 对象<<cout,不符合使⽤习惯和可读性。重载为全局函数把ostream/istream放到第⼀个形参位置就可以了,第⼆个形参位置当类类型对象

1.2赋值运算符重载

赋值运算符重载是一个默认成员函数,用于完成两个已经存在的对象之间的拷贝赋值,这里要注意跟拷贝构造区分,拷贝构造用于一个对象拷贝初始化给另一个要创建的对象。

cpp 复制代码
int main()
{
	Date d1(2024, 7, 29);
	Date d2(2004, 11, 7);

	//赋值重载
	d1 = d2;

	//拷贝构造
	Date d3(d2);
	Date d4 = d2;

	return 0;
}

赋值运算符重载的特点:

  1. 赋值运算符重载是一个运算符重载规定必须重载为成员函数 。赋值运算符重载的参数建议写成const当前类类型引用,传值传参会有拷贝
cpp 复制代码
//d1 = d2;
void operator=(Date d)
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
}

如果这样写,因为赋值前要传参,所以会先调用拷贝构造,再调用复制重载。

  1. 有返回值,且建议写成当前类类型引用(因为传值返回也会先拷贝),引用返回可以提高效率,有返回值目的是为了支持连续赋值场景
  2. 没有显示实现时,编译器会自动生成一个默认赋值运算符重载,默认赋值运算符重载行为跟默认拷贝构造函数类似,对内置类型成员变量会完成值拷贝 / 浅拷贝(一个字节一个字节的拷贝),对自定义类型成员变量会调用它的赋值重载
  3. Date这样的类成员变量全是内置类型且没有指向什么资源,编译器自动生成的赋值运算符重载就可以完成需要的拷贝,所以不需要我们显示实现赋值重载。像Stack这样的类,虽然也都是内置类型,但是_a指向了资源,编译器自动生成的赋值重载完成的值拷贝 / 浅拷贝不符合我们的需求,所以需要我们自己实现深拷贝(对指向的资源也进行拷贝)。像MyQueue这样的类型内部主要是自定义类型Stack成员,编译器自动生成的赋值重载会调用Stack的赋值重载,也不需要我们显示实现MyQueue的赋值运算符重载。如果一个类显示实现了析构并释放资源,那么它就需要显示写赋值重载,否则不需要
cpp 复制代码
#include <iostream>
using namespace std;

class Date
{
public:
	//构造
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	//拷贝构造
	Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

	//赋值运算符重载
	//d1 = d2;
	Date& operator=(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
		return *this;
	}
	
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2024, 7, 29);
	Date d2(2004, 11, 7);

	//赋值重载
	d1 = d2;

	//拷贝构造
	Date d3(d2);
	Date d4 = d2;

	return 0;
}

2、取地址运算符重载

2.1const成员函数

  • const修饰的成员函数称之为const成员函数,const修饰成员函数放到成员函数参数列表的后面
  • const实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。const修饰Date类的Print成员函数,Print隐含的this指针由Date* const this变为const Date* const this
cpp 复制代码
#include <iostream>
using namespace std;

class Date
{
public:
	Date(int year = 2024, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	//void Print(const Date* const this)
	void Print() const
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	//const限定d1不能被修改
	const Date d1(2024, 7, 30);//权限平移
	d1.Print();

	Date d2(2024, 8, 1);//权限缩小
	d2.Print();
	return 0;
}

2.2取地址运算符重载

取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载,一般这两个函数编译器自动生成的就够我们用了,不需要去显示实现,除非一些特殊的场景,比如我们不想让别人取到当前类对象的地址,就可以自己实现一份,胡乱返回一个地址。

cpp 复制代码
class Date
{
public:
	Date* operator&()
	{
		//正常代码
		//return this;
		return (Date*)0x004FF88C;
	}
	
	const Date* operator&() const
	{
		//return this;
		return (Date*)0x004FF88C;
	}

private:
	int _year;
	int _month;
	int _day;
};

3、类型转化

  • C++支持内置类型隐式类型转换为类类型对象,需要有相关内置类型为参数的构造函数
  • 构造函数前加explicit就不再支持隐式类型转换
cpp 复制代码
#include <iostream>
using namespace std;

class A
{
public:
	//explicit A(int a)
	A(int a = 1)
	{
		_a1 = a;
	}

	A(int a1, int a2)
	{
		_a1 = a1;
		_a2 = a2;
	}

	A(const A& aa)
	{
		_a1 = aa._a1;
	}

	void Print()
	{
		cout << _a1 << " " << _a2 << endl;
	}
private:
	int _a1;
	int _a2;
};

class Stack
{
public:
	//临时变量具有常性,用const修饰
	void Push(const A& aa)
	{
		//...
	}
private:
	A _arr[20];
	int _top;
};

int main()
{
	A aa1 = 1;
	aa1.Print();
	
	//隐式类型转换
	//构造一个A的临时对象,再用这个临时对象拷贝构造aa2
	//编译器遇到连续构造+拷贝构造->优化为直接构造
	A aa2 = 2;
	aa2.Print();

	A& raa1 = aa2;
	const A& raa2 = 2;//临时变量具有常性

	Stack st;
	A aa3 = 3;
	st.Push(aa3);//构造一个A类对象再插入

	st.Push(3);//直接插入

	A aa4 = { 4, 5 };
	const A& aa5 = { 4, 5 };
	st.Push(aa4);
	st.Push({ 4, 5 });

	return 0;
}

4、再探构造函数

之前我们实现构造函数时,初始化成员变量主要是使用函数体内赋值,构造函数初始化还有一种方法,就是初始化列表,每个构造函数都有初始化列表。

  • 初始化列表的使用方式是以一个冒号开头,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式
  • 每个成员变量在初始化列表中只能出现一次,语法理解上初始化列表可以认为是每个成员变量定义初始化的地方
  • 引用成员变量、const成员变量,没有默认构造的类类型变量,必须放在初始化列表位置进行初始化,否则会编译报错
  • C++11支持在成员变量声明的位置给缺省值,这个缺省值主要是给没有显示在初始化列表初始化的成员使用的
  • 尽量使用初始化列表初始化 ,因为那些你不在初始化列表初始化的成员也会走初始化列表,如果这个成员在声明位置给了缺省值,初始化列表会用这个缺省值初始化。如果你没有给缺省值,对于没有显示在初始化列表初始化的内置类型成员是否初始化取决于编译器,C++并没有规定。对于没有显示在初始化列表初始化的自定义类型成员 会调用这个成员类型的默认构造函数,如果没有默认构造函数会编译错误
cpp 复制代码
#include <iostream>
using namespace std;

class Time
{
public:
	Time(int hour)
		:_hour(hour)
	{
		cout << "Time()" << endl;
	}
private:
	int _hour;
};

class Date
{
public:
	Date(int& rx, int year = 2024, int month = 1, int day = 1)
		:_year(year)
		,_month(month)
		,_day(day)
		,_n(1)
		,_m(rx)
		,_t(2)
	{
		//...
	}

	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	//这里不是初始化,是缺省值,这个缺省值是给初始化列表的
	//如果初始化列表没有显示初始化,默认就会用这个缺省值初始化
	int _year = 2024;
	int _month = 11;
	int _day = 7;

	//必须在初始化列表初始化
	const int _n = 1;
	int& _m = _day;
	Time _t = 12;//没有默认构造

	int* _ptr = (int*)malloc(4);
};

int main()
{
	int x;
	//对象定义
	Date d1(x, 2024, 7, 31);
	d1.Print();

	//const定义的变量、引用必须在初始化列表定义
	const int x = 1;
	Date& d2 = d1;

	return 0;
}
  • 初始化列表中按照成员变量在类中声明顺序进行初始化,跟成员在初始化列表出现的先后顺序无关,但是建议声明顺序和初始化列表顺序保持一致

| 下面程序的运行结果是什么:

cpp 复制代码
#include <iostream>
using namespace std;

class B
{
public:
	B(int a)
		:_a1(a)
		,_a2(_a1)
	{
		//...
	}

	void Print()
	{
		cout << _a1 << " " << _a2 << endl;
	}
private:
	int _a2 = 2;
	int _a1 = 2;
};

int main()
{
	B b(1);
	b.Print();
	return 0;
}

因为在B类中,先声明的_a2,再声明的_a1,所以在初始化列表中先初始化_a2,再初始化_a1,初始化_a2的时候_a1还是随机值,然后再用a初始化_a1。


5、static成员

  • static修饰的成员变量,称之为静态成员变量,静态成员变量一定要在类外进行初始化
  • 静态成员变量为当前类的所有对象所共享不属于某个具体的对象,不存在对象中,存放在静态区
  • static修饰的成员函数,称之为静态成员函数,静态成员函数没有this指针
  • 静态成员函数中可以访问其他的静态成员,但是不能访问非静态的,因为没有this指针
  • 非静态的成员函数,可以访问任意的静态成员变量和静态成员函数
  • 突破类域就可以访问静态成员,可以通过类名::静态成员 或者对象.静态成员来访问静态成员变量和静态成员函数
  • 静态成员也是类的成员,受public、protected、private访问限定符的限制
  • 静态成员变量不能在声明位置给缺省值初始化,因为缺省值是给构造函数初始化列表的,静态成员变量不属于某个对象,不走构造函数初始化列表

求1+2+3+...+n

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 arr[n];
		return Sum::GetRet();
    }
};

当定义一个Sum类类型长度为n的变长数组时,会调用n次构造,实现n的累加。

| 设已有A、B、C、D四个类定义,程序中A、B、C、D构造函数调用顺序为?析构函数调用顺序为?

cpp 复制代码
C c;
int main()
{
	A a;
	B b;
	static D d;
	return 0;
}

我们知道对象实例化时自动调用构造函数,a、b、c、d四个对象创建的先后顺序是c、a、b、d,所以构造函数的调用顺序就是C、A、B、D;对象在销毁时自动调用析构函数,又规定后定义的先析构,所以局部对象a和b先析构b再析构a,而d是静态的局部对象,生命周期是全局的,所以再析构d,最后析构c, 所以析构函数的调用顺序就是B、A、D、C。


6、友元

友元提供了一种突破类访问限定符封装的方式,友元分为友元函数友元类 ,在函数声明或者类声明的前面加friend,并且把友元声明放到一个类里面。

  • 外部友元函数可访问类的私有和保护成员,友元函数仅仅是一种声明,不是类的成员函数
  • 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
  • 一个函数可以是多个类的友元函数
  • 友元类中的成员函数都可以是另一个类的友元函数,都可以访问另一个类中的私有和保护成员
  • 友元类的关系是单向的,不具有交换性,比如A类是B类的友元,但是B类不是A类的友元
  • 友元类关系不能传递,如果A是B的友元,B是C的友元,但A不是C的友元
  • 有时提供了便利,但友元会增加耦合度,破坏了封装,所以友元不宜多用
cpp 复制代码
// 前置声明,否则A的友元函数声明编译器不认识B
class B;
class A
{
	// 友元声明
	friend void func(const A& aa, const B& bb);
private:
	int _a1 = 1;
	int _a2 = 2;
};

class B
{
	// 友元声明
	friend void func(const A& aa, const B& bb);
private:
	int _b1 = 3;
	int _b2 = 4;
};

void func(const A& aa, const B& bb)
{
	cout << aa._a1 << endl;
	cout << bb._b1 << endl;
}

int main()
{
	A aa;
	B bb;
	func(aa, bb);
	return 0;
}

7、内部类

如果一个类定义在另一个类的内部,这个内部类就叫做内部类。

  • 内部类是一个独立的类,跟定义在全局相比,他只是受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包括内部类
  • 内部类默认是外部类的友元类
cpp 复制代码
#include<iostream>
using namespace std;

class A
{
public:
	class B // B默认就是A的友元
	{
		public :
		void foo(const A& a)
		{
			cout << _k << endl; //OK
			cout << a._h << endl; //OK
		}
	};
private :
	static int _k;
	int _h = 1;
};

int A::_k = 1;

int main()
{
	cout << sizeof(A) << endl;
	A::B b;
	A aa;
	b.foo(aa);
	return 0;
}
  • 内部类本质也是一种分装,当A类跟B类精密关联,A类实现出来主要是给B类使用,那么可以考虑把A类设计为B类的内部类,如果放到private / protected位置,那么A类就是B类的专属内部类,其他地方都用不了

求1+2+3+...+n这个题我们可以这样写:

cpp 复制代码
class Solution {
public:
	int Sum_Solution(int n) {
		Sum arr[n];
		return _ret;
	}
private:
	//
	class Sum
	{
	public:
		Sum()
		{
			_ret += _i;
			++_i;
		}
	};
	static int _i;
	static int _ret;
};

int Solution::_i = 1;
int Solution::_ret = 0;

8、匿名对象

  • 用类型(实参)定义出来的对象叫匿名对象,相比之前我们定义的类型 对象名(实参)定义出来的叫有名对象
  • 匿名对象生命周期只在当前一行,一般临时定义一个对象当前用一下即可,就可以定义匿名对象
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) {
		//...
		return n;
	}
};

int main()
{
	A aa1;

	// 不能这么定义对象,因为编译器⽆法识别下⾯是⼀个函数声明,还是对象定义
	//A aa1();
	
	// 但是我们可以这么定义匿名对象,匿名对象的特点不⽤取名字
	// 但是他的⽣命周期只有这⼀⾏,下⼀⾏他就会⾃动调⽤析构函数
	A();
	A(1);
	A aa2(2);

	// 匿名对象在这样场景下就很好⽤,单纯想调用类中的一个函数
	Solution().Sum_Solution(10);

	return 0;
}

9、对象拷贝时的编译器优化

  • 现代编译器会为了尽可能提高程序的效率,在不影响正确性的情况下会尽可能地较少一些传参和传返回值过程中可以省略的拷贝
  • 如何优化C++标准并没有严格规定,各个编译器会根据自己的情况自行处理,当前主流的相对新一点的编译器对于连续一个表达式步骤中的连续拷贝会进行合并优化,有些更新更激进的编译器还会进行跨行跨表达式的合并优化
cpp 复制代码
#include <iostream>
using namespace std;

class A
{
public:
	A(int a = 0)
		: _a1(a)
	{
		cout << "A(int a)" << endl;
	} 

	A(const A& aa)
		:_a1(aa._a1)
	{
		cout << "A(const A& aa)" << endl;
	} 

	A& operator=(const A& aa)
	{
		cout << "A& operator=(const A& aa)" << endl;
		if (this != &aa)
		{
			_a1 = aa._a1;
			return*this;
		}
	}

	~A()
	{
		cout << "~A()" << endl;
	}
private:
	int _a1 = 1;
};

void f1(A aa)
{}

A f2()
{
	A aa;
	return aa;
} 

int main()
{
	// 传值传参
	A aa1;
	f1(aa1);
	cout << endl;
	// 隐式类型,连续构造+拷⻉构造->优化为直接构造
	f1(1);
	// ⼀个表达式中,连续构造+拷⻉构造->优化为⼀个构造
	f1(A(2));
	cout << endl;
	cout << "***********************************************" << endl;
	// 传值返回
	// 返回时⼀个表达式中,连续拷⻉构造+拷⻉构造->优化⼀个拷⻉构造 (vs2019)
	// ⼀些编译器会优化得更厉害,进⾏跨⾏合并优化,直接变为构造。(vs2022)
	f2();
	cout << endl;
	// 返回时⼀个表达式中,连续拷⻉构造+拷⻉构造->优化⼀个拷⻉构造 (vs2019)
	// ⼀些编译器会优化得更厉害,进⾏跨⾏合并优化,直接变为构造。(vs2022)
	A aa2 = f2();
	cout << endl;
	// ⼀个表达式中,连续拷⻉构造+赋值重载->⽆法优化
	aa1 = f2();
	cout << endl;
	return 0;
}

相关推荐
远望清一色11 分钟前
基于MATLAB的实现垃圾分类Matlab源码
开发语言·matlab
confiself20 分钟前
大模型系列——LLAMA-O1 复刻代码解读
java·开发语言
凌云行者30 分钟前
OpenGL入门005——使用Shader类管理着色器
c++·cmake·opengl
XiaoLeisj32 分钟前
【JavaEE初阶 — 多线程】Thread类的方法&线程生命周期
java·开发语言·java-ee
凌云行者33 分钟前
OpenGL入门006——着色器在纹理混合中的应用
c++·cmake·opengl
杜杜的man35 分钟前
【go从零单排】go中的结构体struct和method
开发语言·后端·golang
幼儿园老大*36 分钟前
走进 Go 语言基础语法
开发语言·后端·学习·golang·go
半桶水专家37 分钟前
go语言中package详解
开发语言·golang·xcode
llllinuuu38 分钟前
Go语言结构体、方法与接口
开发语言·后端·golang
cookies_s_s38 分钟前
Golang--协程和管道
开发语言·后端·golang