【C++】类和对象

目录

前言:

一、定义类

1.struct

访问权限:

 2.class

struct和class区别:

扩展-生命周期:

3.类的实例化: 

定义和声明:

4.this指针

二、类的6个默认成员函数

1.构造函数

2.析构函数

1、2总结:

3.拷贝构造函数

4.operator(运算符、赋值重载)

总结拷贝构造和赋值重载

三、类的其他相关细节

1.初始化列表

2.explicit关键字

3.匿名对象 

4.static成员

5.友元

6.内部类

7.针对连续构造的优化


前言:

        大家好呀,嘿嘿,现在我们即将面对C++的一大拦路虎--类和对象。这将是我们学习C++语法的一大难点,突破此我们才算真正打开C++的学习大门!话不多说,和我一起攻略吧!ᕦ(・ㅂ・)ᕤ

        简述一下本文章要介绍的知识点:struct定义类、class定义类、两种定义类的区别、类的实例化、this指针、类的6个默认成员函数、与类相关的各种优化介绍。

一、定义类

1.struct

        C语言中,(1)struct是定义结构体的关键字,但是此结构体内能声明变量。C++在延续struct作为结构体的概念下同时对其进行了升级,升级成了。也就是说,不仅仅可以声明变量,也可以声明函数并且定义。

        所以,此时struct所在的花括号{}包含的域就有了一个全新的名字--类的作用域。同时,也就出现了访问权限

struct Test
{
	//类的作用域
	int a;//类所含的变量 -- 成员变量
	void test();//类可以包含函数的声明 -- 成员函数
};
//出了类的作用域了
int main()
{
	Test t;//类的实例化
	t.a = 2;//类名点变量 -- 访问实例化类中的属性
	return 0;
}

1.类里面定义的属性前面均称成员(成员变量、成员函数)。

2.类的实例化和C语言中定义结构体变量类似(和定义基本数据类型变量一致),此时可以把类当做一种特殊的类型(后面会介绍)

3.访问类成员:类名.成员名 (和C语言里面访问结构体成员一致)

访问权限:

        这里需要引出的是访问权限。 

        首先这是struct定义类,为了能延续C语言里结构体的功能,所以默认访问类型是public

        那么访问权限是哪些呢?

【C++】类和对象_第1张图片

 C++的访问权限如上。

1.public:正所谓是公共的,所以类外可以访问。

*2.protected、private:一个保护一个私有,细化需要在继承那里才能区分,目前认为两者类外均不可访问。

3.默认是用户不用手动添加上去的。如果需要增加访问权限,只需在行头加上访问权限:即可,直到遇到类域结束或者下一个访问权限符该范围内成员均是此权限。

struct Test
{
private ://私有的
	int _num1;
	void Print1()
	{
		cout << "我在private内哦~\n";
	}
protected://受保护的
	int _num2;
	void Print2()
	{
		cout << "我在protected内哦~" << endl;
	}
public:
	int num3;
	void Print3()
	{
		cout << "我在public内哦~" << endl;
	}
	void test()
	{
		_num1 = 1;
		_num2 = 2;
		Print1();
		Print2();
		cout << _num1 << endl;
		cout << _num2 << endl;
	}
};


int main()
{
	Test te;//类的实例化
	//te._num1;//私有不可访问哦
	//te.Print1();
	//te._num2;//受保护的同样不能在类外访问哦
	//te.Print2();
	te.num3 = 3;
	cout << te.num3 << endl;
	te.Print3();//公共属性就可以在类外访问。public默认公共属性
	te.test();//但是在本类中就可以自由访问成员哦
	return 0;
}

 【C++】类和对象_第2张图片【C++】类和对象_第3张图片

 2.class

        然后就不得不介绍一下常用的定义类的关键字啦~class。这里就可以区分其和struct定义类的不同了。

struct和class区别:

        struct定义类里面的成员默认访问权限是public。

        class定义类里面的成员默认访问权限是private。

  

        然后就来看一下类中声明函数,类外定义函数吧:

class Test
{
	int year;//默认私有属性
public :
	void test();
};

//void test();注意,此时如果函数名前不加上指定的类域的话那么就是定义的普通函数了
void Test::test()
{
	cout << "Test //" << endl;
	year = 2022;//类里可以访问私有属性
	cout << "今夕是何年?" << year << "年喔~\n";
}

int main()
{
	Test t;
	t.test();
	return 0;
}

 需要注意函数类中定义和类外定义的区别:

1.如果函数在类中定义的话,会等同于在函数名前默认置上inline,如果符合内联的要求那么就会直接在调用函数展开。

2.如果在类外定义的话,就不会默认上内联,但是如果定义和声明分明(一般是在两个文件里),就不可内联了。(根据内联的规则,在编译过程中就不会将定义的函数放入符号表,然后链接的时候就找不到对应的函数了,自然就会出现错误)

3.特别的注意访问的就近原则。优先就近,然后到全局或者命名空间去找,否则就是指定的空间去找,比如类名::  或者命名空间名::

扩展-生命周期:

有一个误区,生命周期是由域决定的,即出了域后,某变量的生命周期也就对应的结束了。如果是这样的话,那么传出去的静态变量(static)和malloc开辟的空间又该怎么去解释呢?

所以,得出一个结论:

        域应该只是限制访问权限,真正影响生命周期的是:(函数调用建立栈帧)栈  (直到free)堆 (静态变量,程序结束销毁) 静态区  常量区   ----->即所储存的区域

3.类的实例化: 

        在说类的实例化之前,我们先来区分一下定义和声明的区别。

定义和声明:

        给出如下代码,判断那里是定义哪里是声明。

class Test
{
public:
	int age;
	int year;
};
int year;

        答案就是在类域外的year(全局变量)是定义,并且被赋予了初始值0,但是在Test中的age和year是声明。没有赋予空间(即定义)。

        为什么?因为没有实例化

可以把类当做是图纸,实例化对象就是修房子。图纸自然没有空间,房子却有空间。

1.定义格式:(C++类型分类:内置类型(基本类型)、自定义类型(struct/class))

自定义类型(类名)变量名;

此时,通过这个图纸已经实例化出一个房子了,就拥有了空间了。

int main()
{
	Test t;
	cout << "全局变量year:" << year << endl;
	//cout << "Test类中的year: " << t.year << endl;注意,并没有初始化,只是全局变量会默认进行初始化
	t.year = 1;
	cout << "Test类中的year: " << t.year << endl;

	return 0;
}

        那么,如何计算类中的空间?

        我们知道C语言里面struct计算其空间使用的就是内存对齐,但是没有函数,类中有函数该如何处理呢?

        先看如下的代码:

class Test
{
public:
	void test()
	{
		cout << "这里是Test内哦~" << endl;
	}
	int num;
};

int main()
{
	Test* temp = nullptr;
	temp->test();
	return 0;
}

        这里不就成了空指针引用嘛,不会报错嘛?答案是:不会。

         这说明了什么呢?函数的大小没有放进类里面,自然调用相关类就不需要去类里面找就更不用谈解引用了。函数是放在公共代码区的,和类这一类型相互对应。

2.类的大小:

遵循C语言里struct内存对齐原则。

class Test
{
public:
	void test()
	{
		cout << "这里是Test内哦~" << endl;
	}
	char a;
	int num;
};

int main()
{
	cout << sizeof(Test) << endl;
	return 0;
}

        首先a放在偏移量为0的位置,然后num(4和8比较4小)放在4的位置上,存4 5 6 7 。7不是(8和变量最大的4比4更加小,所以整个类的大小必须是4的整数倍)4的整数倍,所以还需要一格大小,总大小为8.

所以,实例化一个对象,对应类的每一个对象成员都是独立空间,但是不同对象调用的成员函数都是同一个。

int main()
{
	Test a, b;
	cout << "a:" << &a.num  <<  endl;
	cout << "b:" << &b.num << endl;
	cout << "test():" << &Test::test << endl;
	return 0;
}

 

        那么空类,有没有大小呢?

class Test1
{

};
class Test2
{
	void test1()
	{

	}
	void test2()
	{
		cout << "好好内卷呀!(bush" << endl;
	}
};
int main()
{
	cout << sizeof(Test1) << endl;
	cout << sizeof(Test2) << endl;
	return 0;
}

事实证明,空类和只有成员函数的类(没有成员变量) 大小并不是0,而是1(总有个地方需要存储相关位置和指向)

4.this指针

        那么,我们想一下:既然一个类不同对象调用的成员函数是同一个,那么如何区分同一个类的不同对象引用的变量所不同呢?(我们通过上面的知识可以了解到,每个对象的变量均分配的有大小)

        所以,我们就需要一个指向这个对象的指针,并且不能改变,这样方便调用此对象中的变量。为此,C++定义了一个默认看不到的参数(非静态函数 -- 静态函数没有this指针传入),即this指针。(vs下编译器要求不能显示展开 -- 即不能用户自己显示写出来)

非静态函数里规定了第一个参数为隐藏参数this来指向本对象里面的变量。因为成员函数处于公共区域,需要指向本对象内存的变量。

该指针全称:类名*const this (由于const在*的右边,左定值右定向,所以就绑定了this了)

        因为this指针绑定了,所以外界就不能对this干什么,但是内部是可以使用的。虽然内部也是隐式表达,但是也可以显示表达。this->

class Test
{
public:
	void test(int year)
	{
		this->_year = year;
		cout << "今夕是" << _year << "年" << endl;
	}
private:
	int _year;
};


int main()
{
	Test t;
	t.test(2022);
	return 0;
}

         那么既然说到这里了,之前通过空指针引用一个无引用成员变量的成员函数就不得不提了。虽然函数位于公共区域,不需要对象来存储,但是变量要啊。如果访问的函数里面有成员变量会不会成功呢?

class Test
{
	int year;
public:
	void test()
	{
		year = 1;
		cout << "year:" << year << endl;
	}
};
int main()
{
	Test* t = nullptr;
	t->test();
	return 0;
}

        果然,代码不出意料的崩溃了。因为这里函数调用的就是this->year会造成空指针引用哦。

this指针正常情况下存在栈区域,---属于形参
但是有时候不是,vs优化会放在寄存器上exc。比如频繁的调用this指针。

        但是偶然会出现下面的这种情况:

class Test
{
	int _year = 2022;//缺省值 -- 默认构造函数用的(后面会讲)
public:
	void GetYear()
	{
		cout << _year << endl;
	}
};

int main()
{
	const Test t;
	t.GetYear();
	return 0;
}

        此时我们在编译的时候就会遇到如下情况:

         虽然是const,但是我们没有修改里面成员的值呀,这是由于权限放大

在引用那边也特别的讲过了:

对于指针和引用来说(引用本质也是指针),存在权限放大、权限平移、权限缩小的问题。

        因为即使没有修改对象里面的成员,但是别忘了隐藏的this指针,此时将一个不可被修改的对象地址传给一个可被修改值的this指针(const * 给了  *const)(对于指针const位置可以以左定值右定向的口诀来记),所以权限就会放大。解决的方法就是在对应的成员函数后面加上const即可。(因为指针无法显示在参数中写,所以c++就采取了在函数括号后加上const的做法(中间加个空格也行))

        如下,此时就可以编译运行通过: 

【C++】类和对象_第4张图片

二、类的6个默认成员函数

        用户没有显示实现,编译器自动生成的成员函数。具体如下几种:初始化和清理:构造函数、析构函数 拷贝赋值:拷贝构造、赋值重载 取地址重载...

1.构造函数

        如果按照C语言里面的套路,即使我们有C++类相关的语法,初始化类中的变量也会是这样的:

class Test
{
	int _year;
	double _a;
	char _b;
public:
	void Init(int year, double a, char b)
	{
		_year = year;
		_a = a;
		_b = b;
	}
	void Print()
	{
		cout << "Year:" << _year << endl;
		cout << "_a:" << _a << endl;
		cout << "_b:" << _b << endl;
	}
};
int main()
{
	Test t;
	t.Init(2022, 13.4, 'a');
	t.Print();
	return 0;
}

【C++】类和对象_第5张图片

        那么每次创建这个类的同时,必须要记得起调用Init这个初始化函数,否则就会:

【C++】类和对象_第6张图片

        但是,有时就会忘记初始化。有没有一种方法能够解决这样的问题,让我们在实例化类的时候就必须进行初始化呢?------ 构造函数,就此诞生!

 构造函数:用来初始化的。(不是开辟空间构造函数哦,是一个特殊的成员函数)

特殊之处:

1.用户没有显示调用会生成默认构造函数

2.没有返回值(void也没有),名字和类名一致。

3.构造函数同样可以重载。

4.需要const修饰this一样加在函数后面

        有了构造函数,我们就可以将上面的代码改造一下。

class Test
{
	int _year;
	double _a;
	char _b;
public:
	Test(int year, double a, char b)
	{
		_year = year;
		_a = a;
		_b = b;
	}
	void Print()
	{
		cout << "Year:" << _year << endl;
		cout << "_a:" << _a << endl;
		cout << "_b:" << _b << endl;
	}
};

        此时,如果主函数里面还是上面那样的话,会发现Test t处就会报错了,说不存在默认构造函数。(即不传参数的构造函数)

        所以我们需要主动传递参数才能通过编译。这样就确保了实例化对象的时候进行初始化。

int main()
{
	Test t(2022, 13.4, 'a');
	t.Print();
	return 0;
}

【C++】类和对象_第7张图片

         构造函数也是一个成员函数,也可以进行重载和缺省哦。

public:
	Test()//默认构造函数(无参
	{

	}
	Test(int year, double a = 0.0, char b = ' ')//缺省
	{
		_year = year;
		_a = a;
		_b = b;
	}
int main()
{
	Test t1(2022);
	Test t2;
	t1.Print();
	t2.Print();
	return 0;
}

        此时默认也是允许的哦,因为构造函数也可以重载的原因。但是此时如果是全缺省的话就会和无参的构造函数发生冲突了。(系统会分辨不出来该调用哪个函数)

注意默认构造函数有一下三类:

1、我们不写,编译器自动生成的那个
2、我们自己写的,全缺省构造函数
3、我们写自己写的,无参构造函数

特点:定义类对象不传参数均叫默认构造函数  默认构造函数只能存在一个((2 ,3)可以同时存在,符合语法,但是调用会混淆(存在歧义/二义性))

        那么,系统默认构造函数有什么用呢?

class Test2
{
public:
	Test2()
	{
		cout << "Test2" << endl;
	}
};
class Test
{
	int _year;
	double _a;
	char _b;
	Test2 t;
public:
	void Print()
	{
		cout << "Year:" << _year << endl;
		cout << "_a:" << _a << endl;
		cout << "_b:" << _b << endl;
	}
};
int main()
{
	Test t;
	t.Print();
	return 0;
}

        如上程序就会发生一件有趣的事:

【C++】类和对象_第8张图片

        可以看到,系统默认构造函数会帮我们将自定义类型进行调用默认构造函数,但是内置(即基本数据类型)类型的成员均没有进行初始化。 

*但是默认生成的构造函数,对内置类型成员不做处理,对自定义类型的成员会去调用其默认构造函数。(C++早期设计的缺陷,本来内置类型也一并处理,就更简化了) c++11打了一个补丁:即可以在类内,给变量赋值一个初始值,但不是初始化,而是给缺省值。因为在这个里面时声明。

        补丁演示:(实际上属于缺省)

class Test2
{
public:
	Test2()
	{
		cout << "Test2" << endl;
	}
};
class Test
{
	int _year = 0;
	double _a = 0.0;
	char _b = ' ';
	Test2 t;
public:
	void Print()
	{
		cout << "Year:" << _year << endl;
		cout << "_a:" << _a << endl;
		cout << "_b:" << _b << endl;
	}
};

        这样,在进行上述调用之后就会默认帮我们自动初始化啦。

【C++】类和对象_第9张图片

2.析构函数

        按理说,一个对象创建了,在最后回收的时需要清理一下垃圾。

        正常的内置类型(除指针外)无需清理,这里针对的就是在堆内开辟的空间和其他的自定义类型,于是就有了析构这样的一个清洁工。

作用:对象销毁(对象的生命周期结束)时,会自动调用析构函数,完成对象中资源的清理。

函数:~类名(){//清理代码}

        根据上面的说法就是清理我们需要手动清理的,比如堆内存的申请。

class Test
{
public:
	//构造函数
	Test(int num = 1)
	{
		_num = num;
		int* temp = (int*)malloc(sizeof(int) * 4);
		assert(temp);
		_arr = temp;
	}
	//析构函数
	~Test()
	{
		//此类中存在堆内存,如果不释放会造成内存泄漏
		free(_arr);
		_arr = nullptr;
		//方便查看对象销毁是否调用析构函数
		cout << "~Test()" << endl;
	}
private:
	int* _arr;
	int _num;
};

int main()
{
	Test t1(0);
	return 0;
}

         那么析构函数需要注意的有:

1.无参数,无返回值 2.无法重载(没有参数)3.没有显示写,系统会自动生成了默认的析构函数

        同理,那么默认的析构函数有什么用呢?

跟构造函数类似,内置类型不处理,(环境复杂(指针不一定都是malloc一个的,还有其它的),不好处理)自定义类型调用它的默认析构(注意如果显示写自定义类型仍然自动处理)。

class Test1
{
public:
	~Test1()//为了方便查看是否调用析构函数 我们显示写并且打印
	{
		cout << "~Test1()" << endl;
	}
};
class Test2
{
public:
	~Test2()//显示写,同样自动调用自定义类型的析构
	{
		cout << "~Test2()" << endl;
	}
	Test1 t;
};
class Test3
{
	Test1 t;//不显示写使用默认的
};
int main()
{
	Test2 t1;
	Test3 t2;
	return 0;
}

【C++】类和对象_第10张图片

         所以需要默认析构函数的时候就是:适用于全是自定义类型时(比如用栈实现队列那里)

1、2总结:

构造

        1.大部分的类需要自己写构造函数。

        2.一般只有像Myqueue这样的类不需要显示写构造函数(两个栈实现一个队列)
        3.每个类最好都要提供默认构造函数(不用传参数)。(构造函数要频繁的调用。放在类里作为inline)(自己提供的构造函数也会调用自定义类型成员的默认构造函数 -- 后面讲到初始化列表会解释)

析构

        4.一些类需要显示写析构函数(stack,Queue...)-- 即需要释放成员在内存申请的空间。

        5.一些类不需要显示写析构函数。a.Date这样的类,没有资源可以清理b.比如MyQueue也可以不写,默认生成的就可以(和构造函数类似,默认的会自动调用自定义类型的析构函数)
        6.由于函数和变量均是填入栈帧的,(栈和栈帧里面的对象)所以均符合后进先出,也是后定义先销毁/析构(注意析构不是销毁哦~)        栈帧是由ebp、esp两个寄存器控制的
*初始化和销毁:全局和静态局部变量均存在静态区(数据段),全局会在函数没有创建出来就进行初始化。在函数栈帧里的生命周期随着函数而存亡。除此静态区和栈保持一致,先进先初始化,后进先析构然后销毁。

class Test
{
public:
	Test(int num)
	{
		_num = num;
		cout << "Test --" << _num << endl;
	}
	~Test()
	{
		cout << "~Test() --" << _num << endl;
	}
private:
	int _num;
};
Test t1(1);//全局变量
static Test t2(2);//全局静态
int main()
{
	//局部静态
	static Test t3(3);
	//局部变量
	Test t4(4);
	Test t5(5);
	//局部静态
	static Test t6(6);
	return 0;
}

        问:上述构造顺序和析构顺序分别怎样:

1 2 3 4 5 6    5 4 6 3 1 2

【C++】类和对象_第11张图片

3.拷贝构造函数

        当我们想复制一份类对象时,就需要拷贝构造函数了。

调用:类 类名(一个类对象) or 类 类名 = 另一个类对象

是一个特殊的成员函数,是构造函数的一个重载形式

函数:类名(const 类名& d){//各个成员赋值}

const作用:拷贝本身对象数据不会被改变

&作用:不会陷入无穷递归之中。

        首先来解释一下这里参数为什么不是对象而是引用。

class Test
{
public:
	Test(int num = 999)
	{
		_num = num;
	}

	Test(Test d)
	{
		_num = d._num;
	}
private:
	int _num;
};

int main()
{
	Test t1(1);
	Test t2(t1);
	return 0;
}

        如果形参传的是对象(传值传参)的话,会调用拷贝构造函数,既然这样,就会无穷无尽的进行调用,那么还怎么拷贝呢?

        所以上面的应该传引用,并且加上const,即Test(const Test& d);

【C++】类和对象_第12张图片

         那么,默认的拷贝函数会做什么呢?

内置类型,按照字节方式直接拷贝,而自定义类型调用其的拷贝构造完成拷贝的。(浅拷贝/值拷贝)

class Test
{
public:
	Test(int num = 999)
	{
		_num = num;
	}

private:
	int _num;
};

int main()
{
	Test t1(1);
	Test t2(t1);
	return 0;
}

 

        这里就和其他默认成员函数不同了,之前的默认成员函数均不会对内置处理的,这一点记忆的时候需要注意。但是是不是这样我们无论什么环境下都可以不写拷贝构造了呢?

class Test
{
public:
	Test(int num = 1)
	{
		_num = num;
		int* temp = (int*)malloc(sizeof(int) * 3);
		assert(temp);
		_arr = temp;
	}
	~Test()
	{
		free(_arr);
		_arr = nullptr;
	}
private:
	int _num;
	int* _arr;
};

int main()
{
	Test t1(2);
	Test t2(t1);
	return 0;
}

        这个编译完看似没有问题,一旦运行起来就会报错,这是为什么?调试一下便可知:

【C++】类和对象_第13张图片

         我们可以发现就是这个默认拷贝函数导的乱。这样将里面的指针地址拷贝成一样的了,那么两个互不干扰的对象其中一个改变另一个也要改变还不说,销毁时,会调用析构函数两次,导致free两次,自然运行出错。

        这就是浅拷贝的问题,这种问题就需要我们自己写深拷贝去解决了。(深拷贝之后在讲啦~)

4.operator(运算符、赋值重载)

        我们经常在代码里面使用内置类型进行先关的符号操作,比如 int a1, a2  a1 +-*/%= a2...那么,存不存在一种可能,方便我们的代码可读性和操作性,让我们的自定义类也能用上这些符号呢?答案是自然可以。

1.函数:返回类型 operator操作符(参数1, 参数2){//代码实现}(一般均是有两个操作数的)

2.默认成员函数: 赋值重载取地址重载

3.运算符重载规定:
    1.不能链接C++没有的运算符
    2.必须要有一个类类型参数
    3.不能重载的运算符:.*  ::  sizeof  ?:   .

        下面我们以日期类为例:

class Date
{
public:
	Date(int year = 2003, int month = 7, int day = 9)//全缺省
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Printf()
	{
		cout << _year << '/' << _month << '/' << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date a;
	Date b(2022, 8, 6);
	b.Printf();
	a.Printf();
	b = a;
	b.Printf();
	return 0;
}

        观察上述程序,我们只是显示实现了两个函数,但是可以发现b = a可以使用。这就是其中的一个默认成员函数,赋值重载。

        【C++】类和对象_第14张图片

         观察实现结构,就和平时我们理解的一致,将一个变量的值赋给领一个变量。那么如果叫我们显示实现该如何实现呢?利用operator = 就可以实现了。

	Date& operator=(const Date& d)//权限的缩小
	{
		//防止相同的重复赋值:
		if (this != &d)//利用地址进行比较 &也是默认重载
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
		return *this;
	}

        其中,几个细节尤其要注意: 

首先,是在类中实现的。

传引用返回是为了避免传值返回又要调用拷贝构造函数。传引用返回效率更高。(因为返回要是一个自定义类型就要拷贝一个值返回的。注意传值返回的值均是临时变量,具有常性(const)

传入参数也使用引用,因为传值返回同样需要进行拷贝构造。利用const保证传入对象不能改变,并且此时是权限的缩小(Date -> const Date),编译不存在问题。

不要忘了类的非静态成员函数第一个参数是this指针。

        其实赋值和取地址重载默认就已经写好了,其中赋值重载和拷贝构造默认效果类似,内置类型值拷贝,自定义类型调用其的赋值重载。既然也是这样,那么也会面临深浅拷贝问题,所以遇到需要深拷贝,那么也要自己写,而取地址重载就可以不用自己写,默认生成就行,除非不想别人用自己的地址:

        比如当我们没有自己写取地址重载的时候:

int main()
{
    Date a;
	Date* b = &a;
	b->Printf();
    return 0;
}

        此时可以正常运行:

        那么不想别人使用取地址,就可以自己定义一个取地址重载函数,放在私有区即可:

【C++】类和对象_第15张图片

        此时取地址就会出现问题:

总结拷贝构造和赋值重载

        引用实际就是为自定义类型准备的。传值传参需要调用拷贝构造-->构造对象  (但是注意传值和传相同类型的引用,可以构成重载,但是调用会构成歧义)

        *结论:传对象:传引用返回比传对象返回效率更高(需要注意生命周期,即出了此函数就销毁的就存在问题)
    1.一些类需要显示写拷贝和赋值(Stack、Queue)
    2.一些类不需要显示写拷贝和赋值。(a、比如Date这样的类,默认生成就会完成值拷贝/浅拷贝b、比如MyQueue这样的类,默认生成就会调用他自定义类型成员Stack的拷贝和赋值)

        既然 = 、&这些符号就可以针对于自定义类型进行重载,那么常见的运算符也可以重载实现了:

        比如下面代码(注在类里)简单实现一下+=重载:

	//注意:日期类加只有对天数有效,对日期无效哦~
	Date& operator+=(int day)
	{
		_day += day;
		while (_day > DayForMonth(_month))
		{
			_day -= DayForMonth(_month);
			_month++;
			if (_month == 13)
			{
				_month = 1;
				_year++;
			}
		}
		return *this;
	}

        此代码中的DayForMonth函数时计算该年此月份的天数,具体实现如下:

	//提取当月天数:
	int DayForMonth(int month)
	{
		static int day[] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
		day[2] = 28;
		if (month == 2 && 
			(_year % 400 == 0) || ((_year % 4 == 0) && (_year % 100 != 0)))
		{
			day[2] = 29;
		}
		return day[month];
	}

        日期类的相关具体实现细节可以看日期类的实现博客哦~

        然后我们用如下代码测试:

int main()
{
	//测试+=
	Date a(2022, 2, 28);
	a.Printf();
	a += 1;
	a.Printf();
	cout << "-------------" << endl;
	Date b(2000, 2, 28);
	b.Printf();
	b += 1;
	b.Printf();
	cout << "--------------" << endl;
	b += 40;
	b.Printf();
	b += 400;
	b.Printf();
	b += 40000;
	b.Printf();
	return 0;
}

        【C++】类和对象_第16张图片

         答案是准确的哦~这样我们也就可以类比的去实现其他运算符重载啦。同时需要注意的就是在其他运算符重载函数中也可以套用其他运算符重载哦~这样写代码也就比较方便啦~在常规的+-、<>等运算符中,其实只要写> ==、+=、-=其余直接复用即可。

int main()
{
	//没有给自定义类型流插入重载前:
	int year, day, month;
	cin >> year >> day >> month;
	Date a(year, day, month);
	a.Printf();
    return 0;
}

        现在我们会发现,如果要用键盘给日期类赋值的话,会比较麻烦,既然内置类型可以直接使用流插入和流提取,那我们的自定义类型是不是也可以进行实现呢?--这样就可以避免上面使用流插入和成员函数Print了。

        首先要明确cout和cin分别是ostream类型和istream类型的对象,均在官方命令空间std内实现的,那么我们只需要调用此类型对cout和cin针对于我们的自定义类型进行重载不就好了嘛:

	istream& operator>>(istream& in)
	{
		in >> _year >> _month >> _day;
		return in;
	}
	ostream& operator<<(ostream& out)
	{
		out << _year << '/' << _month << '/' << _day;
        return out;
	}

        上面同样是在类中实现的,因为要访问类中的私有成员。但是此时使用cin >> a(Date类型变量)或者cout << a;会报错:

         所以,我们首先要明确并且需要注意的是(上面的运算符重载函数也是)

operator操作符 使用时,在操作符左边对应的是函数实现的时候第一个形参,右边对应的就是第二个参数。(没有就不用写)

因为类的成员函数默认第一个参数是this指针,无法修改。

        所以,上面的实现就默认左操作数就应该是自定义类型了,要实现上面我们重载函数的功能就要这样:

int main()
{
    Date a;
    a >> cin;
	a << cout;
    return 0;
}

 

         虽然可以实现,但是会表现的十分怪异。那么实现此运算符重载就不能放在类里面,但是不放在类里面就无法访问私有成员。这个时候就只能用友元函数了。后面会讲。

三、类的其他相关细节

        类的定义和六个默认成员函数以及运算符重载均以介绍完毕,那么其中的相关细节有哪些呢?

1.初始化列表

        上面讲类的实例化的时候提过,在类里面的均为成员均为声明,在类实例化后里面的成员就会定义。那么好不好奇这个定义是怎么回事呢?

        并且,在构造函数那里存在一个缺陷,当自定义类型初始化的时候会调用其的默认构造。但是当我们想具体初始化一个自定义类型的时候就会出现问题,也就是先进行默认构造,然后我们自己重新定义一个自定义类型,然后在赋值。比较繁琐,如下:

class Test2
{
	int _a2;
public:
	Test2(int a = 0)//默认构造函数
	{
		_a2 = a;
	}
};
class Test1
{
	int _a1;
	Test2 _a;
public:
	Test1(int a1, int a2)
	{
		_a1 = a1;
		Test2 a(a2);
		_a = a;
	}
};


int main()
{
	Test1 a(1, 2);
	return 0;
}

【C++】类和对象_第17张图片

        那么,此时就有必要介绍一下初始化列表了:

在构造函数函数名下方:(没有显示写会默认生成)

格式 类名()

           :成员变量(给其值)                    -- 对于自定义类型同理 ()

           ,....

        {

                // ...

        } 

1.红色部分就是初始化列表,对象成员均在此定义,自定义类型调用其的构造函数。

2.默认不写情况就是内置类型定义给随机值,自定义类型调用其默认构造函数(没有传参数)

3.其实c++11加的补丁就是将缺省值给了初始化列表。

4.三类成员必须使用初始化列表进行初始化:

        没有提供默认构造函数的自定义类型成员

        const成员也必须在初始化列表进行初始化(因为const只有一次机会初始化,所以必须定义的时候同时初始化)

        引用成员

        了解了初始化列表之后,就不用上面的那样繁杂:

	Test1(int a1, int a2)//初始化列表
		:_a1(a1)
		,_a(a2)
	{
	}

        【C++】类和对象_第18张图片

        针对必须要使用初始化列表进行初始化的成员:

        如果不用初始化列表:

class Test2
{
	int _x;
public:
	Test2(int x)//没有提供默认构造函数
		:_x(x)
	{
	}
};
class Test1
{
	Test2 _a;
	const int _b;
	int& _c;
public:
	Test1(int x, int b, int& c)
	{
		Test2 a(x);
		_a = a;
		_b = b;
		_c = c;
	}
};

int main()
{
	//Test1 a(1, 2);
	int c = 3;
	Test1 a(1, 2, c);
	return 0;
}

【C++】类和对象_第19张图片

        所以,在初始化列表进行初始化就好了:

	Test1(int x, int b, int& c)
		:_a(x)
		,_b(b)
		,_c(c)
	{
	}

【C++】类和对象_第20张图片

        此时便就没有问题了。

        那么既然初始化列表如此好用,全用初始化列表岂不是更好?--不一定,因为初始化列表也就提供定义和初始化,当有些不光要定义还要进行判断的时候,初始化列表就显得没那么好用了:malloc

class Test
{
	int* _a;
public:
	Test()
		:_a((int*)malloc(sizeof(int) * 4))
	{

	}
	~Test()
	{
		free(_a);
		_a = nullptr;
	}
};

         虽然可行,但是会别扭,并且无法检测是否返回来的是空值,如果堆内存不够就会出现问题:

	Test()
	{
		int* temp = (int*)malloc(sizeof(int) * 4);
		if (temp == NULL)
		{
			perror("malloc");
			exit(-1);
		}
		_a = temp;
	}

        这样的话就非常好了,所以有些情况也就不用初始化列表了。但是一定要记住初始化列表是定义的地方。

        所以推荐内置类型和自定义类型均使用初始化列表进行初始化,但是也要学会变通,遇到特殊情况就混合食用,效果更佳。

        但是注意一个坑:

class Test
{
	int _a;
	int _b;
public:
	Test(int x)
		:_b(x)
		,_a(_b)
	{
	}
};
int main()
{
    Test a(1);
    return 0;
}

        此时你认为_a,_b的结果是1,1吗?

        结果就是_a是随机值。不要被初始化列表的顺序给欺骗了哦~定义顺序是按照声明的顺序来的,所以即使初始化列表里面_b比_a先,也是_a先初始化的。

2.explicit关键字

        对于直接将内置类型赋给自定义类型的构造,这中间实际发生了隐式类型转换。比如之前学习构造函数的时候 类名 对象名(内置类型值)就可以写成类名 对象名 = 内置类型值

class Test
{
	int _num;
public:
	Test(int num = 0)
		:_num(num)
	{

	}
};

int main()
{
	Test a(1);
	Test b = 2;

	return 0;
}

         为什么可以这么调用呢?知道平时的整形提升如何来的吗?类似存在一个临时变量在中间过渡。

        首先将整形提升为Test类型,发生一次构造,然后将此临时变量拷贝给我们此时定义的b变量。  发生了构造和拷贝构造。

        但是编译器底层对此进行了优化,直接优化成了一次构造

        比如我们在构造和拷贝构造进行信息输出,查看发生了几次构造和几次拷贝。

	Test(int num = 0)
		:_num(num)
	{
		cout << "Test()构造" << endl;
	}
	Test(const Test& t)//拷贝构造
	{
		_num = t._num;
		cout << "Test()拷贝构造" << endl;
	}

         优化状态下确实只调用了一次构造而已:

        那么可以取消这种优化吗?

explicit的用处就是这样。在构造函数前加入就表示此自定义类型不能被隐式转换,自然就无法被优化。

        此时在进行编译就会报错哦~

3.匿名对象 

        所谓匿名对象,即对应的自定义类型没有变量名,生命周期就只有当前这一列

class Test
{
	int _num;
public:
	Test(int num = 0)
		:_num(num)
	{
	}
	~Test()
	{
		cout << "Test()析构" << endl;
	}
	void Print()
	{
		cout << _num << endl;
	}
};

int main()
{
	Test();
	Test(2).Print();
	Test().Print();
	return 0;
}

【C++】类和对象_第21张图片

        适用于只使用类里的函数。

4.static成员

 static静态的。此时申请的内存区域在数据段(静态区)。生命周期也就可程序一致。

静态成员

        如果是静态成员,那么,在初始化列表无法进行初始化,只能在类外进行初始化。但是由于C++语法特性,在static前加上const即就可以直接在类里进行初始化了,就无需在类外进行。静态成员属于此类,该类的所有对象均可以使用。

静态函数

        在成员函数前加上static就是静态函数。此时就不会默认给this指针了。既然没有this指针就无法访问成员变量,只能访问静态成员。而且此时静态函数也可以不用通过对象去调用,直接前面加上类域即可。

        首先,静态成员只能在类外进行初始化,在初始化列表上初始化不了~

class Test
{
	static int _num;
public:
	Test()
		:_num(0)
	{
	}
	void test()
	{
		_num++;
	}
};
int main()
{
	Test t;
	return 0;
}

        所以应该在类外进行初始化:(加上类域即可)

int Test::_num = 0;//初始化定义

         这样就好了,此时该类所有对象共享其,那么每次构造就让其++,如下:

	Test()
	{
		_num++;
	}

        此时在main函数创造一个数组,比如Test t[12],那么进行调试就可以发现_num的值:

         但是如果此时在静态成员前加上const,就无需在类外进行初始化,在类里初始化即可,效果相同。

        静态函数没有默认给的this指针,那么就无法访问该类中成员变量:

class Test
{
	static int _num;
	int _x;
public:
	Test(int x = 0)
		:_x(x)
	{
		_num++;
	}
	static void Print()
	{
		cout << _num << endl;
		cout << _x << endl;
	}
};

        比如上述代码,编译就会出错:

         但是静态成员_num还是可以访问的。并且调用静态函数的时候可以直接通过类域调用:

int main()
{
	Test t[12];
	Test::Print();
	return 0;
}

        当然,如果放在private内也就无法进行访问了。 

5.友元

        正所谓friend,即友好关系,适用于函数和类。

友元函数

        此函数可以访问此类中的所有成员。

        声明方式:在类中使用friend加在此函数前进行声明即可。

友元类

        此时这个类可以访问声明友元的类里的所有成员。但是注意友元关系是单向的,即声明友元的类就不可访问对应的这类里面的私有、保护成员。

        首先,我们来填上上面在用流提取流插入重载的坑,因为需要cin、cout作为第一个参数,所以需要定义成全局函数,但是为了能够访问类中的成员,所以此时在日期类里声明为友元函数即可:

class Date
{
	//一般友元关系声明在第一行:
	friend inline ostream& operator<<(ostream& out, const Date& d);
	friend inline istream& operator>>(istream& in, Date& d);
    //......
}
//声明为内联 -- 节省栈帧
inline ostream& operator<<(ostream& out, const Date& d)
{
	out << d._year << '/' << d._month << '/' << d._day << endl;
	return out;
}

inline istream& operator>>(istream& in, Date& d)
{
	in >> d._year >> d._month >> d._day;
	return in;
}

        此时就可以达成目的啦:

        友元类是如何呢?

class Test1
{
	friend class Test2;//友元类
	int _num;
public:
	Test1(int num = 0)
		:_num(num)
	{

	}
};
class Test2
{
	Test1 t;
public:
	void Print()
	{
		cout << t._num << endl;
	}
};

int main()
{
	Test2 t;
	t.Print();
	return 0;
}

        此数友元类就可以访问声明友元的类的所有成员了,当然,友元是单向的,不可以反过来。

6.内部类

        所谓内部类,就是在一个类内部继续声明一个类。

一个类定义在另一个类的内部,此时默认此类就是外面类的友元类
注意:外部类和内部类大小不冲突,除非成员含有其类成员。 --

内部类:1.受外部类类域限制,外面想调用必须是公有部分并且加上访问限定符。(和静态成员函数类似)2.内部类天生就是外部类的友元。

        外部类和内部类的大小不冲突:

class Test1
{
	int _a;
public:
	Test1(int a)
		:_a(a)
	{

	}
	class Test2
	{
		int _b;
	public:
		Test2(int b)
			:_b(b)
		{}
		void Print(Test1& d)
		{
			cout << d._a << endl;
		}
	};
};

int main()
{
	cout << sizeof(Test1) << endl;
	return 0;
}

        此时根据内存对齐原则,外部类中只有一个成员变量有效:

         并且内部类只受类域的限制,并且天生为外部类的友元类:

int main()
{
	Test1 a(1);
	Test1::Test2 b(0);
	b.Print(a);
	return 0;
}

7.针对连续构造的优化

         上面大概c++的类与对象相关基础语法介绍完毕,现在我们来看看一些底层编译器进行的优化:

        分析如下程序,试试说出调用了几次构造和几次拷贝构造:

class Test
{
public:
	Test()
	{
		cout << "Test()构造" << endl;
	}
	Test(const Test& t)
	{
		cout << "Test()拷贝构造" << endl;
	}
};

Test f()
{
	Test t;
	return t; 
}

int main()
{
	Test a = f();
	return 0;
}

        首先调用f(),进入函数内部,首先就构造一个Test类型的t对象,执行了一次构造,然后要返回t,因为此时t为局部变量,生命周期只有在此函数内部,出了函数就会被释放,那么此时应该需要创建一个临时变量接受一次拷贝构造,然后此临时变量就给外面Test a处一次拷贝构造。所以综合起来就是1次构造,两次拷贝构造。

【C++】类和对象_第22张图片

        可是当我们调用的时候:

         结果只调用了一次拷贝构造。这就是编译器的优化:当连续构造然后拷贝构造的时候,编译器会直接优化成一次构造,在上面的栈帧压栈结构中,实际步骤如下:

【C++】类和对象_第23张图片

        形如上面的优化还有这样:

void f2(Test t)
{

}

int main()
{
	Test f2(Test(1));//  构造 + 拷贝构造   -- 优化 构造               同一个步骤 
	return 0;
}

       (上面为了方便调用构造,在Test类中添加了一个成员变量)

         此时中间是一个匿名对象,本身应该先执行一次构造,然后传入参数里面进行一次拷贝构造,连续的一步之类进行了两次构造,便就优化成了一次构造。

结论:连续一个表达式步骤中,连续构造一般都会优化成一个构造。--合二为一

在编译器底下,debug优化相对于release跟轻,release更加猛,在Linux下的g++编译器里debug就优化的更猛,优化需要注意。 

你可能感兴趣的:(C++,c++,学习)