C++入门第四篇----详解C++类和对象知识点

前言:

在本篇文章中,我们首先要接着前2篇中关于类和对象的内容继续往下,C++的类和对象应该是我开始学习程序以来第二个感觉到很难完全掌控和灵活使用的知识点(上一个是文件操作和预处理部分),故在这里我会花费大量的时间去理解和分析类和对象的语法特点和知识体系,有问题的地方倘若各位发现请及时指出。
在这里插入图片描述
首先我们依旧拿出这张大图,上面代表着类的6个默认的成员函数,他们的特点是:用户自己显式定义的时候会使用用户显式定义的,当用户没写的时候就会使用编译器自己默认生成的,这就是他们的共同特点。

1.赋值重载续operator=:

在上一篇文章中,我们介绍了运算符重载,它解决了C++对于自定义类型使用常规的内置类型的运算符的方式,而在类中默认的是赋值运算符重载。
注意细节:我们赋值重载的返回值应该仍然是类对应的类型的引用返回,这样才能满足赋值运算符连续赋值的功能,如下:

date& operator=(date& dd)//赋值重载函数
	{
		cout << 3 << endl;
		_a = dd._a;
		_b = dd._b;
		_c = dd._c;
		return *this;
	}

1.问题一:赋值运算符和拷贝构造有什么区别呢?

我们不妨拿下面的例子来看:

class date
{
public:

	date(int a = 10, int b = 20, int c = 30)//构造函数(全缺省)
	{
		cout << 1 << endl;
		_a = a;
		_b = b;
		_c = c;
	}
	date(date& dd)//构造拷贝函数
	{
		cout << 2 << endl;
		_a = dd._a;
		_b = dd._b;
		_c = dd._c;
	}
	date& operator=(date& dd)//赋值重载函数
	{
		cout << 3 << endl;
		_a = dd._a;
		_b = dd._b;
		_c = dd._c;
		return *this;
	}
	~date()//析构函数
	{
		cout << 4 << endl;
	}
private:
	int _a;
	int _b;
	int _c;
};
int main()
{
	date q1;
	date q2 = q1;
	cout << "----------------" << endl;
	date q3;
	cout << "----------------" << endl;
	q3 = q1;
	cout << "----------------" << endl;
	return 0;
}

在这里,我们重点来看q2=q1以及q3=q1的区别,为了表示我们的变量赋值过程中进入了哪些函数,我们在每个函数内部都打印一个数字作为标识。打印的结果如下:
在这里插入图片描述
我们发现一个特别有趣的现象,即q2=q1调用的不是我们的赋值重载运算符,而q3=q1调用的是我们的赋值重载运算符,出现这样问题的原因在于我们的编译器的优化,同时它也反映了赋值重载和拷贝的区别,首先,我们的q2是不存在的,我们是创建一个q2让其等于q1,常规的过程应该是首先创建一个q2对象然后调用赋值重载函数把q1的值给q2,但在仔细一想,我们的构造和赋值完全可以用一个拷贝构造代替,故编译器就为我们将其优化为了一个拷贝构造,再看q3,q3首先是自己先创建出来的对象,然后我们让q3=q1,这里由于不涉及到先创建q3再调用赋值重载的问题,故我们直接调用赋值重载函数赋值即可。
由此,我们总结出来:
拷贝构造是一个已经存在的对象去拷贝初始化另一个对象,而赋值重载是两个已经存在的对象,一个给另一个赋值,再选择调用哪个时也是优先考虑对象是否已经被创建!!!

2.问题二:默认的operator=是如何使用的呢?

倘若我们不写赋值重载,编译器会自动默认生成一个赋值重载函数,跟拷贝重载的的行为类似,默认的operator=对内置类型会完成值拷贝,而对于自定义类型会调用它的赋值重载函数(显式或非显式),
故根据这一条,我们总结出:不需要开辟空间的,只进行浅拷贝的就不需要写赋值重载函数,而涉及到开辟空间的,由于会出现多次释放的问题,故必须自己写赋值重载函数,让其进行深拷贝而不是简单的传值的浅拷贝!!!!!这条结论很关键,要反复思考形成一种行为的反射。
故我们可以总结:构造和析构行为类似,而拷贝构造和赋值重载行为类似。

3.问题三:运算符重载的使用意义以及如何更加规范的使用运算符重载!!!

举一个简单的例子,我们判断a>b可以写一个函数,那a<=b是不是就是a>b反过来呢?这就是运算符重载的一个重要的思路:复用,由于反复进行自定义类型的各种操作符,代码的不仅多而且十分冗长,故我们完全可以利用一个完整写下来的函数来进行逻辑复用,就像我上面举得例子一样:下面让我们来看一个实例:

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;
	}
}
bool Date::operator==(const Date& d) const//==运算符重载
{
	if (_year == d._year && _month == d._month && _day == d._day)
	{
		return true;
	}
		return false;
}
bool Date:: operator != (const Date& d) const//!=运算符重载
{
	return !(*this == d);
}
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);
}

在这里,我仅仅写下了> == 的逻辑,就利用逻辑复用解决了剩下的全部比较操作符,这个复用思路在书写C++类的函数的时候都适用,应当反复思考成为自己的一种代码思路去进行,减少重复代码的书写,更加提高效率。

4.问题四:前置符号和后置符号如何在自定义类型赋值重载中区分呢?!!!

根据我们的赋值重载的概念可知,operator表示++,只能是operator++,同理–也是如此,那怎样才能做到区分呢?
我们可以联想到我们学到的函数重载的知识,我们是如何做到区分同名函数的呢?没错,通过让参数不同从而进行区分,故对于后置运算我们统一在参数的括号里面加一个对应的数据类型(int,且只能是int,别的数据类型是不允许的)如下:

Date& Date::operator++()//前置++
{
	*this +=1;
	return *this;
}
Date Date::operator++(int)//后置++,注意,由于前置加加和后置加加的运算符重载是相同的写法,故为了区分,我们采用重载函数的方式,给后置加加补上一个int类型,这样就可以区分前置和后置了
{
	Date q(*this);
	*this +=1;
	return q;
}
Date Date::operator--(int)//后置--
{
	Date q(*this);
	*this -=1;
	return q;
}
Date& Date::operator--()//前置--
{
	*this -=1;
	return *this;
}

我这里以这个例子,在这里我们发现我的后置统一在系数加了一个int,从而达到了区分前置和后置的区别,然后在这里,我直接对自定义类型加减是因为我前面实现了一个关于+ -的运算符重载,如下:

Date& Date::operator+=(int day)//日期+=天数
{
	if (day < 0)//为了处理我们输入一个负数,导致我们对应的天数出现负数的bug的情况
	{
		return *this -= (-day);
	}
    _day += day;
	while (_day > GetMonthDay(_year, _month))
	{
		_day -= GetMonthDay(_year, _month);
		_month++;
		if (_month == 13)
		{
			_year++;
			_month = 1;
		}
	}
	return *this;
}
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;
}

故这里不要被迷惑,自定义类型是不能对其自己进行运算符重载的,还是要自己写出对对应的函数,后续再使用前置和后置自定义计算的时候,只要像内置类型那样去使用即可。
!!!!在最后,需要注意的一点:赋值重载运算符是不能作为全局函数使用的,只能在类里书写,因为在全局书写则类里面也会自动生成一个,这就导致编译器不知道应该使用哪个好了,所以赋值重载必须作为成员函数使用!!!

5.函数运算符重载的一个重要作用!!!

倘若我们想要创建一个顺序表,并且按照C语言那种常规的遍历去打印一遍顺序表,我们会面临一个问题,如下:

class List
{
private:
	int* _arr1;
	int _size;
	int _capacity;
public:
	List(int size = 0, int capacity = 4)
		:_arr1(nullptr),
		_size(size),
		_capacity(capacity)
	{
		_arr1 = (int*)malloc(sizeof(int) * _size);
		if (_arr1 == nullptr)
		{
			perror("malloc failed");
			exit(-1);
		}
	}
	void Backpush(int x) 
	{
		_arr1[_size++] = x;
	}
};
#include<iostream>
using namespace std;
int main()
{
	List q1;
	q1.Backpush(1);
	q1.Backpush(2);
	q1.Backpush(3);
	q1.Backpush(4);
	for (int i = 0; i < q1._size; i++)
	{
		cout << q1._arr1[i] << endl;
	}
	return 0;
}

报错信息:
在这里插入图片描述

在这里我们想遍历一遍顺序表,但由于我们的private的限制,我们是没法在类外部直接访问里里面的成员的,那我们要是想访问又该如何修改呢?如下,让我们添加两个函数:

int size()
{
	return _size;
}
int& operator[](int i)
{
	return _arr1[i];
}

通过第一个函数我们可以带回来我们顺序表的元素个数,通过第二个函数的赋值重载我们可以把每一个元素带回来,这是我们之前直接访问所做不到的,但有了这两个函数我们就可以这样写:

for (int i = 0; i < q1.size(); i++)
{
	cout << q1[i] << " ";
}

结果为:
在这里插入图片描述

2.const成员:

将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
常见的用法如下:

Date Date::operator-(int day) const// 日期-天数
{
	Date q(*this);
	q -= day;
	return q;
}

即在函数的声明的后面加上一个const

1.注意const成员函数其实就是在修饰隐藏的this指针,而不是你传入的其他参数,哪怕你再传入一个对应类的对象的引用来,你依旧可以修改它的成员的数值,但你是没法修改*this对应的成员数值的,如下

在这里插入图片描述
在这里,你会发现,我们传入一个对象的引用,哪怕是加上const,依旧是可以改变它里面的成员的,但当我们想改变this对应的成员的时候,就会为我们进行错误红线的提示,且报错的内容如下:
在这里插入图片描述
此时我们的
this是不能轻易改变的。

2.注意:权限的问题

注意,我们由const修饰的对象,它是不能进入到非const修饰的成员函数里面的,那样属于是将const对象的权限放大了,在前面的知识中我们知道,权限在计算机中只能缩小或者平替,但不能放大,但我们的非const的对象由权限规则,既可以进入const修饰的成员函数,也可以进入非const修饰的成员函数,如下:

class date
{
public:

	date(int a = 10, int b = 20, int c = 30)//构造函数(全缺省)
	{
		cout << 1 << endl;
		_a = a;
		_b = b;
		_c = c;
	}
	date(date& dd)//构造拷贝函数
	{
		cout << 2 << endl;
		_a = dd._a;
		_b = dd._b;
		_c = dd._c;
	}
	date& operator=(date& dd)//赋值重载函数
	{
		cout << 3 << endl;
		_a = dd._a;
		_b = dd._b;
		_c = dd._c;
		return *this;
	}
	~date()//析构函数
	{
		cout << 4 << endl;
	}
	void change(date& qq) const
	{
		qq._a = 12;
		qq._b = 13;
		qq._c = 20;
	}
private:
	int _a;
	int _b;
	int _c;
};
int main()
{
    date q1;
	date q2 = q1;
	cout << "----------------" << endl;
	date q3;
	cout << "----------------" << endl;
	q3 = q1;
	cout << "----------------" << endl;
	date q4;
	q1.change(q4);
	const date q4;
	q4 = q2;
	return 0;
}![在这里插入图片描述](https://img-blog.csdnimg.cn/370ab321096042e1bc6bec386cdfd1f7.png#pic_center)

我们看报错的结果如下:
在这里插入图片描述
在这里,q1使用由const修饰的函数change是可以的,但由const修饰的对象q4是没法使用没有由const修饰的成员函数operator=的,这便是权限的问题
**同理,我们在const修饰的成员函数内是不能调用非const的成员函数,这是由于我们的*this的对象本身倘若不是const还好,但倘若是const类型的对象,根本没法进入非const修饰的函数内部,这样就发生了权限的扩大。
但相应的,非const函数内部是可以存在由const修饰的成员函数的,因为进入非const成员函数一定是非const的成员对象,这类是可以进入const的成员函数的,属于是权限的缩小,**如下:

void Print()
{
	cout << 1 << endl;
}
void mycopy(date& qq) const
{
	Print();
}

报错信息是:

在这里插入图片描述

3.注意:const的位置不同,其代表的意思也是不同的

我们常见的const位置有两个地方,在一个函数中:
例如:

 int func() const
{
     /;
}

或者是:

const int func() const
{
     //;
}

那么,这两个函数是相同的么?
显然,它们是不同的,其原因在于,第一个const的位置所修饰的是this指针的对象,而第二个是针对返回值进行处理的,第一个const保证了我们的this指针指向的对象是不能被修改的,而第二个指针保证了我们函数的返回值是不能被修改的,这是两种完全不同的情况,第一种暂且不说了,让我们继续分析第二种:
那么,第二种的const有必要加么?
我们之前已经学到,函数传值返回时,倘若数据出了作用域就被销毁的话,我们的返回值是会被临时拷贝一份临时的变量作为返回值返回,而由我们之前学到的,临时变量是具有常属性的,故其实本质上我们的返回值本身就是带上const的,再加上反而是多此一举,但倘若我们是引用返回,我们的返回值本身就是存在的,相应的它的自身属性也不会被改变,故这个时候我们要根据需要是否看这个引用返回的对象是否需要加上常属性const,从而让其不被修改

4.同一个函数。在后面加上const与不加的会构成重载么?

这个问题,我们首先回忆我们函数重载的知识点:在函数重载中,我们知道,编译器处理识别时是根据函数参数的不同来将其标识为不同的个符号,从而让函数构成重载,而在这里我们加上const与不加上const的这两种给了编译器识别的方式,故他们两个是可以构成重载的,而且不同的对象会根据自身的特点去选择调用哪个函数,比如const类型的对象就会调用const成员函数,而非const的对象就会优先调用非const修饰的成员函数。
那这样的函数重载有何意义呢?
大多数情况下,这样写的意义不大,但依旧拿我们的顺序表为例子:

  const int& operator[](int i) const
	{
return _arr1[i];
	}
	int& operator[](int i)
	{
return _arr1[i];
	}
	for (int i = 0; i < q1.size(); i++)
{
	q1[i]++;
	cout << q1[i] << " ";
}
for (int i = 0; i < q1.size(); i++)
{
	q1[i]++;
}

你会发现,我们的q1[i]++变的可以被修改了,按理来说,返回const类型的引用应该是不能被修改的,但由于我们重载了一个operator[]的函数,无论是能否改变的q1[i],它都会自动匹配到对应的重载函数中,故对于读和写分离的函数来说,写两个重载一个只读一个只写是最为合适的。

3.取地址&运算符重载和const修饰的&运算符重载(不是特别重要)

正如我在前面所说的,取地址运算符重载也是默认的成员函数,编译器可以默认生成,而我们只需要直接使用即可,故我们平时根本不需要显式写出取地址运算符重载函数,但倘若你不想让他人通过取地址符号找到对应的地址,我们就可以显式实现,如下:

int* operator&()
{
	return nullptr;
}

这样,一旦使用取地址符号,就会取到空指针而不是本来的自定义类型的指针了。
以上就是我们的类和对象的6种默认函数的全部,下面我们让我们讲一讲类和对象的一些其他的知识点:

4.构造函数补充知识点(重要)----初始化列表:

在构造函数知识点位置,我们曾经提到过初始化列表,默认构造函数是会调用它的自定义类型的默认构造函数的,但假如其自定义类型成员是显式构造函数,而不是默认构造的情况下,编译器是不会让其通过运行的,但我们同时还想自定义让其不调用默认而是调用我们想要赋予的数值,想要解决这个问题,就要使用我们的初始化列表

1.初始化列表的形式:

本质上,初始化列表就是构造函数的一部分,它是在构造函数的函数体前加上一块为成员变量赋值的过程,如下:

class Date
{
    public:
    Date(int year,int month,int day)
        :_year(year),
        _month(month),
        _day(day)
        {
           //;构造函数的函数体内容
        }
};

注意构造函数的几个书写的注意点:
1.构造函数的成员赋值用的是括号而不是等号
2.在函数体里仍可以调整类的成员的数值
3.我们所谓的C11支持给类的成员加上缺省值,实际上就是将缺省值先赋给初始化列表,然后通过初始化列表来进行赋值
4.对于仍然需要动态开辟的成员来说,动态开辟最好还是写在构造函数的函数体里最好,这样方便我们去书写开辟检验的检查程序

2.对初始化列表的理解:

初始化列表可以理解为是每一个成员定义的地方,这与给缺省值的声明是不同的,初始化列表用于解决当成员中由const/引用这写创建的时候必须为其初始化并且我们还想给其我们想给的对象的时候,将其放在缺省值仅仅是声明,相当于没赋值,对于引用和const来说这是没法通过的,故我们利用初始化列表对这些特殊成员进行定义处理,从而解决了问题,也包括自定义成员,在没有默认构造的情况下,在初始化列表可以对其显式构造传参起到了定义的作用,这很好的解决了我们上面的问题。比如我们可以在初始化列表阶段给自定义类型这样赋值a(1),这样我们就可以随心所欲的构造初始化我们的自定义成员
总结:我们可以通过调试的过程发现:初始化列表是每一个成员定义的地方,不管你写或者不写,每一个成员都要走初始化列表,内置类型赋随机值,自定义类型调用其默认构造,且要理清关系,初始化列表就是构造函数的一部分,它的本质是为其提供两种不同的初始化方式。

C++11支持的给缺省值,本质上就是将这个缺省值给初始化列表进行定义的,如果初始化列表没有显示给值,就用缺省值,如果显示给值了,就不用这个缺省值。

初始化列表我们需要注意的点:
1.每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
2.类包含以下成员必须将其放在初始化列表的位置进行初始化:
A.引用成员变量
B.const成员变量
C.自定义类型成员变量(且改类没有默认的构造函数显性调用或者我们想要为其赋值的时候)
3.尽量使用初始化列表进行初始化,因为不管你是否使用初始化列表,对于自定义类型的成员变量,一定会使用初始化列表进行初始化,但特殊情况下要使用初始化列表与函数混合使用(动态开辟)
4.成员变量在类中的声明次序就是其在初始化列表阶段对成员变量定义初始化的顺序,与其在初始化列表的顺序无关,只与声明顺序有关

第四点,我们的可以通过调试得出答案。

5.单参数构造函数的隐式类型转换:

注意其条件,它适用于只含有一个参数的构造函数,也就是说,我们初始化的时候只为其赋一个值,无论是缺省值也好抑或是手动赋值也好好。
例如:下面的例子:

#include<iostream>
using namespace std;
class A
{
private:
	int _a;
	char _c;
	double _b;
public:
	A(int a)
		:_a (a)
	{
		cout << 1 << endl;
	}
};
int main()
{
	A a1(1);
	A a2 = 1;
	const A& a3 = 3;
	return 0;
}

在这里,我们用三种方式创建,其结果为:
在这里插入图片描述
我们发现他们都可以通过并且都调用了构造函数,第一个形式不多说了,是很常规的构造方式,第二个形式是因为首先会调用一个构造函数生成a2,然后再将1赋值给a2,对于编译器来说不如直接将1作为参数直接构造初始化a2,故它直接调用一次构造就出来了,第三个形式是由于3本身为常量,拷贝的过程中的临时变量具有常属性,所以拿const修饰的变量去接收即可。
这样的隐式类型转换有的时候会让人感觉很乱,这种隐式类型转换有时候是可以避免的,我们可以使用explicit在构造函数前,加上之后就不支持隐式类型转换了,如下:

#include<iostream>
using namespace std;
class A
{
private:
	int _a;
	char _c;
	double _b;
public:
	explicit A(int a)
		:_a (a)
	{
		cout << 1 << endl;
	}
};
int main()
{
	A a1(1);
	A a2 = 1;
	const A& a3 = 3;
	return 0;
}

报错如下:
在这里插入图片描述
C++11中不仅支持单参数,也支持多参数的隐式转换,用{ }符号来写入参数,若不想发生,同样也写explicit即可,这一点与单参数相同,如下:

#include<iostream>
using namespace std;
class A
{
private:
	int _a;
	char _c;
	double _b;
public:
	 A(int a,int c,int b)
		: _a(a),
		  _b(b),
		  _c(c)
	{
		cout << 1 << endl;
	}
};
int main()
{
	A a1{ 1,2,3 };
	A a2 = { 10,20,30 };
	return 0;
}

6.匿名对象:

匿名对象即是不拿名字,直接拿类型创建出来的对象,和常规的有名对象不同,有名对象的作用域在当前的局部域,而匿名对象的作用域仅仅在这一行中了,它的特点是调用一次构造后又会调用一次析构,注意最重要的一点,匿名对象也是一种临时变量,故具有常属性

1.匿名对象的作用

用匿名对象可以起到传参数的作用,可以让代码更加的简化,因为有时我们定义一个对象就是为了调用函数,为了一个函数而去创建一个变量,不如直接使用匿名的对象去调用函数,这样就根本不用定义一个对象就能直接调用函数。
实例程序如下:

#include<iostream>
int s = 0;
using namespace std;
class A
{
private:
	int _a;
	char _c;
	double _b;
public:
	 A(int a=10,int c=20,int b=30)
		: _a(a),
		  _b(b),
		  _c(c)
	{
		cout << 1 << endl;
		s++;
	}
	 ~A()
	 {
		 cout << 1 << endl;
	 }
};
int main()
{
	const A& x = A(1, 20, 3);
	A a1(1,2,3);
	A a2 = { 10,20,30 };
	A arr1[10] = { A(1,2,3),A(20,30,40),A(52,60,20)};
	cout << s << endl;
	return 0;
}

比如在这里,我们完全可以利用一个数组来统计s的数值的增加,或者是为了调用某个值而匿名对象,比如说在这里就是为了得到s的累加而构建许多个匿名对象去处理。

2.匿名对象的一种延长生命周期的写法:

看下面的程序(注意匿名对象具有常属性,也就是具有const的属性限制,故拿非const去接收是不可以的)

#include<iostream>
using namespace std;
class A
{
private:
	int _a;
	char _c;
	double _b;
public:
	 A(int a,int c,int b)
		: _a(a),
		  _b(b),
		  _c(c)
	{
		cout << 1 << endl;
	}
	 ~A()
	 {
		 cout << 1 << endl;
	 }
};
int main()
{
	const A& x = A(1, 20, 3);
	A a1(1,2,3);
	A a2 = { 10,20,30 };
	return 0;
}

在这行代码中,通过调试,你会发现第一个匿名对象在调用完构造函数之后没有立刻调用析构函数,也就是说,引用接收的匿名对象实际上延长了它的生命周期,让它有了名字而不是匿名的了吗,这便是匿名对象延长生命周期的写法。

7.static成员:

声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量,用static修饰的函数,称之为静态成员函数,静态成员变量一定要在类外进行初始化。

1. static修饰的特点:

1.由static修饰的静态成员为所有类成员所共享的,不属于某个具体的对象,存放在静态区,不纳入计算对象的大小
2.静态成员变量必须在类外进行定义,定义时不添加static关键字,类中只是声明,且不要给缺省值
3.类静态成员可以用类名::静态成员/对象.静态成员来访问,这也进一步证实了静态成员变量属于这个类,也同时被所有这个类的对象所共享。
4.静态成员函数没有隐藏的this指针,不能访问任何非静态成员(这也进一步说明了其没有this指针)
5.静态成员虽然不属于哪个具体的类,但依旧受类的public,protected,private访问限定符的限制

#include<iostream>
using namespace std;
class A
{
private:
	int _a;
	char _c;
	double _b;
	static int s;
public:
	 A(int a=10,int c=20,int b=30)
		: _a(a),
		  _b(b),
		  _c(c)
	{
		cout << 1 << endl;
		s++;
	}
	 ~A()
	 {
		 cout << 1 << endl;
	 }
};
  int A::s = 100;//这表示,这个全局变量是由全部这个类所共享的,故要加访问限定符
int s = 0;
int main()
{
	const A& x = A(1, 20, 3);
	A a1(1,2,3);
	A a2 = { 10,20,30 };
	A arr1[10] = { A(1,2,3),A(20,30,40),A(52,60,20)};
	int m = sizeof(A);
	cout << m << endl;
	return 0;
}

如下,我们这里的s即为对应的静态成员变量,别忘了,定义静态成员变量的时候一定要加上A::的访问限定符号,要不然没法让编译器知道你这是在这个类里面的静态成员变量。
如下:

 void static Func()
 {
	 cout << "hello world" << endl;
	 _a = 30;
 }

报错信息为:
在这里插入图片描述
这里便证实了,静态成员函数里面是没有隐藏的this指针的,故我们访问对应的成员是非法的。但这一条也同时说明哪怕是匿名对象也可以使用静态成员函数,因为根本不能访问里面的成员,只要在返回类型后加static即可

8.友元:

友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。我们还是提倡
友元分为:友元函数和友元类

1.友元类的通用写法:

friend 类+对象名称/friend 函数声明
友元的意义即声明一块位置与当前的类建立一个联系,让对应的类或者函数可以直接访问友元里面的成员变量。
我们举出下面的一个例子:
使用友元进行<< >>的运算符重载:

void operator<<(ostream& out)
{
   out<<
}
void operator>>(istream& in)
{
   in>>
}

这样写,我们会遇到一个很关键的问题,成员变量都是私有的,没法访问变量,故我们在这里使用友元来让其可以访问。
如下:

class A
{
   friend void operator<<(ostream& out) const;
}

注意,友元是可以放在类的任意位置的,不受条件限定符的限制。
注意写全局变量时,都要用引用,这样作用于本体,不需要调用一次拷贝构造的过程。

9.内部类:

如果一个类定义在另一个类的内部,这个类就叫做内部类,内部类是一个独立的类,和外部类都是独立的,在计算对象大小的时候相互不把对方计入到自己的成员里面,我们是不能通过外部类的对象去访问内部类的成员,外部类对内部类没有任何优越的访问权限,想要访问内部类,必须通过函数给内部类的成员带出来。
注意:内部类是外部类的友元类,内部类可以通过外部类的对象参数直接访问外部类的所有成员,但外部类不能使用内部类的成员,只能由函数带出来。注意,想要创建一个内部类的对象,要加上外部类的访问限定符,例如:
A::这样的访问限定符,要不然没法创建。

1.内部类的特性:

1.内部类可以创建在外部类的public,protected,private都是可以的
2.注意内部类可以直接访问外部类中的static成员,不需要外部类象/类名
3.sizeof(外部类)=外部类,和内部类没有任何关系
内部类受外部类的类域和访问限定符的限制,其他他们两个是独立的类,倘如B类在A类的public,可以经过访问限定符创建B类,但倘若B不在public,是访问不到B的,必须通过创建函数才能创建B类。

如下:

#include<iostream>
using namespace std;
class A
{
private:
	int _a;
	char _c;
	double _b;
	static int s;
	class B
	{
	private:
		int s = 0;
	public:
		void print()
		{
			cout << 1 << endl;
		}
	};
public:
	 A(int a=10,int c=20,int b=30)
		: _a(a),
		  _b(b),
		  _c(c)
	{
		cout << 1 << endl;
		s++;
	}
	 ~A()
	 {
		 cout << 1 << endl;
	 }
	 void static Func()
	 {
		 cout << "hello world" << endl;
	 }
	 void 
};
  int A::s = 100;//这表示,这个全局变量是由全部这个类所共享的,故要加访问限定符
int s = 0;
int main()
{
	const A& x = A(1, 20, 3);
	A a1(1,2,3);
	A a2 = { 10,20,30 };
	A arr1[10] = { A(1,2,3),A(20,30,40),A(52,60,20)};
	int m = sizeof(A);
	cout << m << endl;
	A::B q;
	return 0;
}

报错信息为:
在这里插入图片描述

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

1.既构造又拷贝构造时,编译器按直接构造处理优化,但C++并没有规定优化,主要看编译器
故一个表达式,连续的步骤里,连续的构造会被合并
2.传值返回:用未定义的对象接收时,直接给这个未定义的对象一次拷贝构造,而不用先拷贝再拷贝构造两次了

总结:

以上便是我对类和对象的全部理解,其实还有很多地方理解十分的粗糙和肤浅,但我会继续努力,争取在C++上更进一步。

  • 4
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值