IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> 类和对象(跑路人笔记)<完> -> 正文阅读

[C++知识库]类和对象(跑路人笔记)<完>

面向对象和过程的初步介绍

C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。

C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。

类其实类似于我们C语言中的结构体,不过有对类进行了较多的修改使其更加优秀.

在C语言中我们只能向里面放变量,但是在类里我们可以向里面放函数

如下就是一个类

class date
{
public:
	void Init(int year = 1, int  month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _day;
	int _month;
	int _year;
};

C++习惯使用class来创建类,不过你是用struct来创建类也是可以的=.=,最好使用class

而且C++中我们可以不用typedef来省事了直接可以用类名来创建变量.

如我们就可以直接使用

int main()
{
	date a;
	a.Print();
	return 0;
}

类的定义

class className
{
 // 类体:由成员函数和成员变量组成
}; // 一定要注意后面的分号

类中的元素称为类的成员:类中的数据称为类的属性或者成员变量; 类中的函数称为类的方法或者成员函数。

一般向类中放函数可以有两种情况

  1. 声明定义都放在类中
  2. 值将声明放在类中,把定义放在.cpp的文件里

其中声明定义都放在类中的就如我们下面举的例子一样.

class date
{
public:
	void Init(int year = 1, int  month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _day;
	int _month;
	int _year;
};

这样放入类中的函数,编译器默认其为内联函数.不过内联函数是建议具体要看编译器.

第二种情况

类似下面代码

image-20220515222600414

我们一般推荐第二种方式.

(本文为了方便讲解先使用第一种方法)

我们类里的函数那怕跟其他的函数重名重类型也是可以使用的,我们通过linux来看看

image-20220520220321150

所以我们并不用担心重名情况.

类的访问限定符与封装

访问限定符

如果我们在C语言中使用结构体来写栈,如果有人不通过我们的函数接口来改变变量也是可以做到的,但是这样写会有很多隐患.

如下图

image-20220516105753853

我们好好的写好的代码就这么被人随便改了,我们后续再写代码的时候直接就一堆错误,非常痛苦.

于是我们的C++语言就加入了public private protected这三个来保护权力

image-20220516112151894

  1. public修饰的成员在类外可以直接被访问
  2. protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
  3. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
  4. class的默认访问权限为private,struct为public(因为struct要兼容C)

注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别

class date
{
public:
	void Init(int year = 1, int  month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _day;
	int _month;
	int _year;
};

我们的public下到private之间的内容是都是类之外可以访问的.

而我们private到类定义结束的区间都是我们类之外不能访问的.

类的实例化(对象)

我们想要访问类里的任何元素必须通过对象才可以访问

和我们的C语言结构体一样我们的类也只是一个蓝图而已,他并没有在建造类的时候占据空间,只有在形成变量的时候才会占据空间.

也就是说我们创建的类其实就类似于一个类型,一个自己定义的类型.

我们把用类创建的变量叫做对象.

image-20220516133643504

如何计算类的大小

我们的类中既有函数又有变量那么实例化之后的类的大小如何计算呢?

其实和C语言的结构体相同都是用对齐来计算大小的,我们的类虽然内部可以定义和声明函数但是函数并不占据类实例化后的大小.

而我们在类中的函数其实是存储在公共内存中的,无论对象是否相同

image-20220516134403888

就像我们不在类里的函数一样也都是开辟一次之后就在那一片内存中使用.

image-20220516134841672

image-20220516140328491

所以在类中的函数也是和普通函数一样放在一块公共区域中,然后我们将传入的参数压进去进行操作.

也就不放在类中计算大小了.

如果我们不放变量那么类的大小应该是多大呢?

image-20220517100510480

答案是:1—这个1的意思并不是保存了什么,而是为了占位表示对象存在.

this指针

  1. this指针的类型:类类型* const
  2. 只能在“成员函数”的内部使用
  3. this指针本质上其实是一个成员函数的形参,是对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。
  4. this指针是成员函数第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递

这个指针被我们的编译器隐藏起来了.

在上面我们定义函数的时候你可以看见

class date
{
public:
	void Init(int year = 1, int  month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _day;
	int _month;
	int _year;
};

我在定义Print函数的时候直接写了_year,_month,_day而我们打印的时候也雀氏会打印这些变量保存的内容,其实原因也很简单我们的C++通过隐藏this指针来得到的元素内容.

其实Print这个函数里面的内容应该是这样的.

void date::Print(date* const this)
{
	cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
}

因为是在类里所以private的元素也是可以访问的.

this是C++自己创立自己传参自己修饰,我们只需要写_year这种变量名即可.

this指针也跟普通形参一样存储在栈中

this指针其实是可以为空的.只要不在函数内解引用this指针就不会报错.

如下:

image-20220516170420299

但是如果函数内对this指针进行了解引用就不可以了.

image-20220516170554998


类<2>

类的6个默认生成函数

类如果是个空类我们的编译器,也会生成6个默认的函数并在符合条件的情况下自己调用.这些函数包括

构造函数,析构函数,拷贝构造,赋值重载,两个取地址重载

这6个函数都是我们可以进行改造的并且在使用的时候编译器会自己调用,非常舒服.

image-20220517101340404

构造函数

比如我们现在创建了一个日期型类,我们想对其进行初始化,但是如果我们只是写了个初始化的函数我们还需要每次使用都调用,非常不方便.

而我们的构造函数作为一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有一个合适的初始值,并且在对象的生命周期内只调用一次

值得注意的是,虽然名字叫构造函数但是他跟对象实例化构造没啥关系,构造函数就只管给成员附上值.

特性

  1. 函数名与类名相同。

  2. 无返回值。 (且不是void而是没有返回的类型)

  3. 对象实例化时编译器自动调用对应的构造函数。

  4. 构造函数可以重载。

因为可以重载所以可以创建多个来方便我们使用

  1. 在类被其他类引用是构建函数是会被调用.
class date
{
public:
    //构造函数
	date(int year = 2002, int month = 8, int day = 26)
	{
		_year = year;
		_month = month;
		_day = day;
	}
    //打印函数
	void Print()
	{
		cout << _year << "_" << _month << "_" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	date a;
	a.Print();
	return 0;
}

上面代码实现的结果如下图:

image-20220517151513701

我们这样创建的构造函数是可以传参的

传参形势如下:

image-20220517152354275

上面的构造函数我们使用了全缺省,但是如果我们不使用全缺省会发生什么呢?

我们将构造函数改成下面形势

image-20220517151648789

改成上面形势后就会报下面的错误说我们没有默认构造函数使用.

那么什么可以成为默认构造函数呢?

image-20220517151721357

[第五点介绍](# 特性)第五点的意思直接看图吧=.=

首先建立一个测试类并搭建好他的构建函数

image-20220517161630654

在另一个类中使用测试类

image-20220517161843736

来看看我们测试类的构造函数有没有被调用

image-20220517162058785

调用了.

默认构造函数

直接告诉大家: 只有全缺省,无参,编译器自动生成的可以做默认构造函数,一个类没有默认构造函数并且没有传参的话是实例化出对象的.

但是如果没有默认构造函数,只要传参得当也是可以实例化出对象的.

比如下面的半缺省.

image-20220517153008548

image-20220517153052243

不过我们在搭建默认构造函数的时候还是使用全缺省较好.

而且全缺省的函数和无参不能同时出现,不然我们在使用的时候编译器无法识别.

注意: 我们类成员变量在取名的时候最后前面加上_(不同公司规定不同,反正最好不要直接使用对应名称如year,不然可能会出现以下情况)

image-20220517154927788

因为我们的编译器的this指针是编译器自己调用的,并不是十分智能,所以我们最好还是在前面加上_或者在其他地方加上标识.

也可以用this指针来弄,不过给人感觉怪怪的=.=

析构函数

概念

析构函数也不是将类内成员都销毁那是编译器干的事情,析构函数是在对象的生命结束要被销毁的时候自动调用的函数,比如我们的栈类要向堆区要空间,我们就可以在此处进行归还

特征

  1. 析构函数名是在类名前加上字符 ~.
  2. 无参数无返回值.
  3. 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数.
  4. 对象生命周期结束时,C++编译系统系统自动调用析构函数.
  5. 在当类一中有其他类的时候,类一的对象在被销毁前会调用其他类的析构函数.

来个例子看看吧

class Stack
{
public:
	//构造函数
	Stack()
	{
		_data = (int*)malloc(sizeof(int) * 4);
		_top = 0;
		_capacity = 4;
	}
	//析构函数
	~Stack()
	{
		free(_data);
		_top = 0;
		_capacity = 0;
	}
private:
	int* _data;
	int _top;
	int _capacity;
};

拷贝构造函数

概念

用于将一个相同类型的对象内容拷贝到另一个对象中,只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。

如果是已存在的拷贝到已存在的是

举例如下

class date
{
public:
    //构造函数
	date(int year = 2002, int month = 8, int day = 26)
	{
		_year = year;
		_month = month;
		_day = day;
	}
    //拷贝构造函数
	date(const date& c)//也是构造函数的重载
	{
		_year = c._year;
		_month = c._month;
		_day = c._day;
	}
    //打印函数
	void Print()
	{
		cout << _year << "_" << _month << "_" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

这样我们对另一个对象进行初始化时就可以直接传来对象了.

image-20220517172721754

在初始化时使用=赋值也可以调用到拷贝构造函数

image-20220519121638469

其实我们不写拷贝构造函数我们的编译器也会给我们生成一个,自己生成的拷贝构造函数在浅拷贝的时候完全够用了,当我们需要深拷贝的时候就可以自己写.

深拷贝

既然我们不写拷贝构造函数编译器会自动生成一个用于浅拷贝的,为啥还要有呢?

主要是因为我们要写深拷贝.

深拷贝就是我们在实现的时候注意一下,不能简单通过赋值操作来拷贝的需要深拷贝一下.

比如指针等.

我们比如我们使用Stack类的时候需要从堆区拿空间,就需要指针来保存变量,如果我们使用浅拷贝就会造成free两次的警告,而且在使用的时候也十分诡异.

所以我们要专门写个拷贝构造函数来达到深拷贝的目的.

比如我们的Stack类的.

	Stack(const Stack& s)
	{
		_data = (int*)malloc(sizeof(int) * (s._capacity));
		memcpy(s._data, _data, sizeof(int) * s._capacity);
		_capacity = s._capacity;
		_top = s._top;
	}

我们的这些默认生成的函数都可以自己实现,只要自己记住他们的格式就好.

其中取地址重载等函数都可以实现但是,没必要编译器的实现以及足够我们使用了.

操作符重载

我们用类的时候总要使用-,+,=,*,/,++,--,==,!=等操作符,还是以日期类为例.

当然日期类就一般不会使用*,/了=.=

早操作符重载的格式如下

//返回值类型+operator+要重载的符号+(形参)
     Date& operator=(Date& d1 ,const Date& d2);
//上方就是我对=号赋值符的一次实现.

而具体的我们要看实现.

比如我们可以选择在类里实现或是在类外实现.

先来一个日期类吧

class date
{
public:
	void Init(int year = 1, int  month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _day;
	int _month;
	int _year;
};

我们先用类外的方式实现一个==看看吧

bool operator==(const Date& d1, const Date& d2)
{
	if (d1._year == d2._year && d1._month == d2._month && d1._day == d2._day)
	{
		return true;
	}
	else
	{
		return false;
	}
}

这个其实并不能使用因为我们_year是私有形势存储的,要想使用就必须将_day,_month,_year公有化或者使用内部共有函数来得到他们的值,因为我们只是演示,所以我暂时先把他公有化出来.

我们实现好了这个==运算符后

image-20220520114906104

上图框起的两种方式都可以使用到我们的==重载功能,但是我们偏向使用下面的a==b.

在我们使用a == b的时候其实编译器会帮我们换成第一种的形式—operator==(a,b).

不过我们还是把这些运算符放在类里较好,及保证了类的封装性,又保证了我们元素不被外界访问.

我们将操作符重载放在类里需要对格式进行稍微改变.

	// ==运算符重载
	bool operator==(const Date& d)
	{
		if (_year == d._year && _month == d._month && _day == d._day)
		{
			return true;
		}
		else
		{
			return false;
		}
	}

在类内定义的格式其实和外面定义的几乎没啥区别,除了少了一个变量,在使用_year的时候我们不用把它变成共有化的了.

其实少的那个变量使用了this指针来代替.而_year其实也是通过this指针得到的.

但是我们的this是两个变量的那个呢?

来看看我们如何使用类里定义的重构函数就知道了.

image-20220520152914905

我们在使用的时候的形势其实注定了,上面的①中的this其实就是a的地址.

而下面的②其实this也是a的地址.

两者在编译看来没有区别.编译器也会2变成1后,然后编译.

如果有两个变量一般是左边的左this指针,三个变量就是最左的是this指针

类比思考一下,我们的前置后置++ --等其实在类内实现的时候就没必要专门设置形参只需要一个this指针就够了.但是这样就没办法分别了,所以我们规定后置类型要创建一个int类型的形参用于函数分辨.以便形成重构.

下面是对日期类所有操作符重载的实现,我们在实现的时候,有些部分可以操作符可以复用最好复用,复用带来的好处有很多,不仅方便,而且后期找bug也可以减少低级错误.

class Date
{
public:
	void Print()
	{
		cout << _year << "_" << _month << "_" << _day << endl;
	}

	// 获取某年某月的天数
	int GetMonthDay(int year, int month)
	{
		if (month > 12)
		{
			cout << "月输入错误" << endl;
			return -1;
		}
		int const arr[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };//保存每个月的日期
		if (month == 2 && JudgeLeapYear(year))//闰年
		{
			return 29;
		}
		return arr[month];
	}
	// 全缺省的构造函数
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	// 拷贝构造函数

  // d2(d1)
	Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	// 赋值运算符重载
  // d2 = d3 -> d2.operator=(&d2, d3)
	Date& operator=(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
		return *this;
	}
	// 析构函数
	~Date()
	{
		;//不写用默认的也可
	}
	// 日期+=天数
	Date& operator+=(int day)
	{
		*this = *this + day;//复用+
		return *this;
	}
	// 日期+天数
	Date operator+(int day)
	{
		Date tmp(*this);
		(tmp._day) += day;
		while (tmp._day >= GetMonthDay(tmp._year, tmp._month))//判断我们的day是不能超过当月的最大数值的.
		{
			tmp._day -= GetMonthDay(tmp._year, tmp._month);
			tmp._month += 1;
			if (tmp._month == 13)
			{
				tmp._month -= 12;
				tmp._year += 1;
			}
		}
		return tmp;
	}
	// 日期-天数
	Date operator-(int day)
	{
		Date tmp(*this);
		(tmp._day) -= day;
		while (tmp._day <= 0)
		{
			tmp._day += GetMonthDay(tmp._year, tmp._month);
			tmp._month -= 1;
			if (tmp._month == 0)
			{
				tmp._month += 12;
				tmp._year -= 1;
			}
		}
		return tmp;
	}
	// 日期-=天数
	Date& operator-=(int day)
	{
		_day -= day;
		while (_day <= 0)
		{
			_day += GetMonthDay(_year, _month);
			_month -= 1;
			if (_month == 0)
			{
				_month = 12;
				_year -= 1;
			}
		}
		return *this;
	}
	// 前置++
	Date& operator++()
	{
		*this += 1;
		return *this;
	}
	// 后置++
	Date operator++(int)//传的int是语法规定
	{
		Date tmp(*this);
		*this += 1;
		return tmp;
	}
	// 后置--
	Date operator--(int)
	{
		Date tmp(*this);
		*this -= 1;
		return tmp;
	}
	// 前置--
	Date& operator--()
	{
		*this -= 1;
		return *this;
	}
	// >运算符重载
	bool operator>(const Date& d)
	{
		if (_year > d._year || (_year == d._year && _month > d._month) || (_year == d._year && _month == d._month && _day > d._day))
		{
			return true;
		}
		else
		{
			return false;
		}
	}
	// ==运算符重载
	bool operator==(const Date& d)
	{
		if (_year == d._year && _month == d._month && _day == d._day)
		{
			return true;
		}
		else
		{
			return false;
		}
	}
	//>=运算符重载
	inline bool operator >= (const Date& d)
	{
		if (*this > d || *this == d)
		{
			return true;
		}
		else
		{
			return false;
		}
	}
	// <运算符重载
	bool operator < (const Date& d)
	{
		return !(*this >= d);
	}
	// <=运算符重载
	bool operator <= (const Date& d)
	{
		if (*this < d || *this == d)
		{
			return true;
		}
		else
		{
			return false;
		}
	}
	// !=运算符重载
	bool operator != (const Date& d)
	{
		return !(*this == d);
	}
	// 日期-日期 返回天数
	int operator-(const Date& d)
	{
		Date min = (*this > d ? d : *this);
		Date max = (*this > d ? *this : d);
		int ret = 0;
		while (min != max)
		{
			min++;
			ret++;
		}
		return ret;
	}
    
private:
	bool JudgeLeapYear(int year)
	{
		if ((year % 4 == 0 && year % 100 != 0) || year % 100 == 0)
		{
			return true;
		}
		else
		{
			return false;
		}
	}
private:
	int _year;
	int _month;
	int _day;
};

还有&的重载但是这个一般使用编译器自动生成的就好.格式和前面的一样.

class Date
{ 
public :
 Date* operator&()
 {
 return this ;
 }
 const Date* operator&()const
 {
 return this ;
 }
private :
 int _year ; // 年
 int _month ; // 月
 int _day ; // 日
};

值得提一嘴的是,我们const修饰指针的时候也是可以组成函数重载的,被const修饰的指针在C++的命名规则下会加上一个K.

const成员

将const修饰的类成员函数称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this 指针,表明在该成员函数中不能对类的任何成员进行修改

image-20220520160827670

其实很简单,就是在类内函数()后加上const就可以把原来类型为Date*的this指针变成const Date*类型.

const修饰的变量是不能传给没有被const修饰的引用和指针的,因为我们不能const修饰的变量被修改.

被const修饰的变量传给没有被const修饰的指针和引用时被称为权限放大是不被允许的.

而没被const修饰的变量,传给被const修饰的指针和引用时被称为权限的缩小是被允许的.

如我们在const修饰的this指针函数中无法调用没有被const修饰的.

image-20220523100152376

原因也很简单,我们的函数在传参的时候会默认把this指针也传给调用的函数,我们上述的例子就是把Print函数的this指针传给了GetMonthDay函数,但是Print的this指针是const而GetMonthDay函数的不是const,这种传参属于是权限的放大.

但是非const就可以传给const这是权限的缩小.

初始化列表

我们使用构造函数函数,其实并不是对其进行初始化,而是对其进行赋值

有点像我们的构造函数和下面类似

int a;
a = 10;

但是我们有的时候必须是要对变量进行初始化,而不是赋值.

所以我们就需要到初始化列表了.

经过初始化列表进行初始化的就是直接类似:int a = 10;

初始化列表的格式如下:

初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。

class Date
{
	Date(int year = 2002, int month = 8, int day = 26)
		:_year(year)
		, _month(month)
		, _day(day)
	{
		;
	}
private:
	int _year;
	int _month;
	int _day;
};

如下

image-20220523182327127

注意:

  1. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)

  2. 类中包含以下成员,必须放在初始化列表位置进行初始化

  3. 引用成员变量

  4. const成员变量

  5. 自定义类型成员(该类没有默认构造函数)

  6. 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使 用初始化列表初始化。

  7. 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关

    例子如下:

class test
{
public:
	test()
		:_a1(1)
		, _a2(_a1)
	{
		//cout << "test()" << endl;
	}
	void Print()
	{
		cout << "_a1:" << _a1 << endl;
		cout << "_a2:" << _a2 << endl;
	}
private:
	int _a2;
	int _a1;
};
int main()
{
	test a;
	a.Print();
	return 0;
}

运行结果如下:
image-20220523231012524

而如果我们先初始化_a2在通过_a2 传给_a1就是正常情况,如下:

image-20220523231713383

explicit关键字

介绍explicit之前让我们先看一串代码吧.

class test
{
public:
	test(int a = 1, int b = 1)
	{
		_a = a;
		_b = b;
	}
	void Print()
	{
		cout << "_a>:" << _a << endl << "_b>:" << _b << endl;
	}
private:
	int _a;
	int _b;
};
int main()
{
	test a;
	a = { 10,20 };//特殊的赋值,对代码的可读性不好
	a.Print();
	return 0;
}

浅提一下我们这种特殊赋值的实现是通过先建立一个临时的类再通过拷贝构造传给我们的对象a.我们新的编译器会对这个过程进行优化具体请看[小拓展—编译器优化](# 小拓展—编译器优化)

我们的explicit关键字就可以避免这个问题.

image-20220524200424647

在构造函数前面加上explicit就将那种赋值给避免了.

小拓展

image-20220524200543305

我们的这个赋值方式的成功是我们的编译器在赋值之前创建了一个临时变量,然后通过临时变量与类型a通过拷贝构造将10,20的值给到a中.

static成员

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

面试题:实现一个类,计算中程序中创建出了多少个类对象。

#include<iostream>
using namespace std;
class A
{
public:
	A()
	{
		++_scount;
	}
	A(const A& t)
	{
		++_scount;
	}
	static int GetACount()//静态修饰的成员函数没有this指针
	{
		return _scount;
	}
private:
	static int _scount;
};
int A::_scount = 0;//对类内进行初始化,不能在类内初始化
void TestA()
{
	cout << A::GetACount() << endl;//静态成员函数可以这样调用.
	A a1, a2;
	A a3(a1);
	cout << a1.GetACount() << endl;
}

上述代码中是因为static修饰的成员变量是整个类共享的不会因为换了个对象而改变自身值.

特性

  1. 静态成员为所有类对象所共享,不属于某个具体的实例

  2. 静态成员变量必须在类外定义,定义时不添加static关键字

  3. 类静态成员即可用类名::静态成员或者对象.静态成员来访问

  4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员

  5. 静态成员和类的普通成员一样,也有public、protected、private3种访问级别,也可以具有返回值

  6. 而非静态的成员函数可以调用静态的成员函数.

    image-20220525081356148

C++11成员初始化更新

C++11支持非静态成员变量在声明时进行初始化赋值,但是要注意这里不是初始化,这里是给声明的成员变 量缺省值。

image-20220526091655587

他的作用类似于给初始化列表一个缺省值.

友元

友元可以分为1. 友元类 2.友元函数

使用友元我们可以突破封装,但是也会增加耦合性,所以友元要尽量少用

友元函数

我们在重载操作符的时候有两个操作符<< >>无法重载因为我们的重载调用的时候会把左边的当做this指针,而我们需要把cout放在左边所以我们就需要用的我们的友元函数了.

注: cout只是我们的一个调制好的ostream类的一个全局对象.内置类型的打印已经写好了,所以我们只需要把类里需要打印的通过内置类型打印好即可.

还是以日期类为例子吧:

我们实现一个日期类的<< 来看看吧.

class Date
{
public:
	Date(int year = 2002, int month = 8, int day = 26)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	ostream& operator<<(ostream& out)
	{
		out << _year << "_" << _month << "_" << _day << endl;
		return out;
	}
private:
	int _year;
	int _month;
	int _day;
};

这种实现我们要使用的话十分别扭.

int main()
{
	Date a1;
	a1 << cout;//日期类的<<的使用
	return 0;
}

我们就可以用到友元来实现

实现如下:

class Date
{
public:
	friend ostream& operator<<(ostream& out, Date& d);
	Date(int year = 2002, int month = 8, int day = 26)
	{
		_year = year;
		_month = month;
		_day = day;
	}

private:
	int _year;
	int _month;
	int _day;
};
ostream& operator<<(ostream& out, Date& d)//返回这样的类型是因为<<的结合性是从左到右
{
	out << d._year << "_" << d._month << "_" << d._day << endl;
	return out;
}
int main()
{
	Date a1;
	cout << a1;//正确的使用
	return 0;
}

所以友元的使用也是很容易看出来的.

友元函数的使用就是在类里将函数声明前加上friend 即可.

当我们的函数是类的友元的时候我们就可以访问类的私有空间.

所以友元在函数方面的作用其实就是让一个普通函数具有访问一个类私用空间的权利

注意

  • 友元函数可访问类的私有和保护成员,但不是类的成员函数
  • 友元函数不能用const修饰
  • 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
  • 一个函数可以是多个类的友元函数
  • 友元函数的调用与普通函数的调用和原理相同

友元类

友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。

举例如下:

class Date
{
public:
	friend class test;
	Date(int year = 2002, int month = 8, int day = 26)
	{
		_year = year;
		_month = month;
		_day = day;
	}

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

class test
{
public:
	void Print()
	{
		cout << a._year << endl;
	}
private:
	Date a;
};

上面的例子我们的test就是Date的友元类,我们就可以通过在test里创建Date的对象来访问Date类里的私有成员.


注:

  • 友元关系不能传递.

    如A是B的友元 B是C的友元 但是A不是C的友元

  • 友元不具有交换性.

    比如我们test是Date的友元类所以我们的test可以访问Date的私有成员但是我们的Date不能访问test的私有成员.

内部类

概念: 一个类定义在另一个类的内部就叫内部类.

注意: 内部类是外部类的友元类但是外部类对内部类没有任何优先和权力,对于外部类来说内部类就跟普通的类一样,外部类没有内部类的任何特权.

并且内部类可以不通过对象等做到直接访问外部类的成员(包括:枚举成员(就是C的枚举),static)

特性:

  1. 内部类可以定义在外部类的public、protected、private都是可以的。
  2. 注意内部类可以直接访问外部类中的static、枚举成员,不需要外部类的对象/类名。
  3. sizeof(外部类)=外部类,和内部类没有任何关系。
class A
{
public:
	class B
	{
		void Print(const A& a)
		{
			cout << a.h << endl;
			cout << _a << endl;
            //上述代码均可以通过
		}
	};
private:
	int h;
	static int _a;
};
int A::_a = 10;

我们想要访问到类B的话就需要通过A::B() 的方式进行访问了.

小拓展(关于类名+()这个匿名对象)

我们还是弄个日期类吧

class Date
{
public:
	Date(int year = 2002, int month = 8, int day = 26)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "_" << _month << "_" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

ok,现在我们讲一下匿名对象.

创建方式其实很简单类名+()即可如我们的日期类就可以写成Date()这就是个匿名对象.

匿名对象特性

  1. 生命周期只有一行,运行完就没了.(除非用常引用来接收才可以)

我们一般用它来调用类里的函数之类的.

int main()
{
	Date().Print();
	return 0;
}

具体功能咱也不知道.等后续遇见了再补.

小拓展—编译器优化

我们的新的胆大的编译器会把一下需要构造再拷贝构造的或者双次拷贝构造的操作简化成一次拷贝构造或一次构造.(不同编译器不同)

举例来看

注: 例子都是通过以下代码进行的测验

#include<iostream>
using namespace std;

class test
{
public:
	test(int a1 = -1, int a2 = -1)
	{
		cout << "test()" << endl;
		_a1 = a1;
		_a2 = a2;
	}
	test(const test& a)
	{
		cout << "test(const test& a)" << endl;
	}
	void Print()
	{
		cout << "_a1: " << _a1 << endl;
		cout << "_a2: " << _a2 << endl;
	}
private:
	int _a1;
	int _a2;
};
test fun(test a)
{
	return a;
}

先构造再拷贝构造

int main()
{
	test a1 = 1;//特殊赋值
	return 0;
}

这个特殊赋值我们在讲述[explicit关键字](# explicit关键字)的时候浅提了一下

具体过程我用下面的图片来解释:
image-20220526085626626

我们的编译器优化后就会省略很多步骤,如下图:

image-20220526090356687

而我们在VS2022也是符合的

image-20220526090428312

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-06-01 15:00:47  更:2022-06-01 15:01:48 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 16:53:38-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码