【C++系列P4】‘类与对象‘-三部曲——[类](2/3)

 前言

  • 大家好吖,欢迎来到 YY 滴 C++系列 ,热烈欢迎!
  • 【 '类与对象'-三部曲】的大纲主要内容如下

  • 如标题所示,本章是【 '类与对象'-三部曲】三章中的第二章节——类章节,主要内容如下:

目录

一.类

1.类的组成与计算类的大小(含结构体内存对齐规则)

二. 空类的大小

三.内部类

四.类的六个默认成员函数

1.构造函数

一.什么时候需要自己写构造函数? 

二.构造函数可以使用重载和不可以使用重载的情况

2.析构函数 

一.什么时候需要自己写析构函数? 

3.拷贝构造函数  

一.什么时候需要自己写拷贝构造函数?   

二.默认拷贝构造(浅拷贝)的缺陷:

4.运算符重载函数

一.运算符重载函数和构造函数使用区别:

5.赋值重载函数

6.取地址与取地址重载

五.初始化列表

一.初始化列表和构造函数的关系

二.初始化列表基本结构 

三.初始化列表使用场景

四.尽量使用初始化列表初始化

五.成员变量在初始化列表中的初始化顺序


一.类

  1. C++兼容C,C语言中的结构体strcut也算是一种类,是public(公有)的,可以被类外直接访问。
  2. 类中的函数默认是内联函数,具体是否是内联函数编译器会判断。如果将其定义和声名分开,即类放在.h文件,定义函数放在.cpp文件,函数不为内联函数

1.类的组成与计算类的大小(含结构体内存对齐规则)

  •  访问限定符划分类中既有成员变量,又有成员函数

 

计算类的大小,只用考虑成员变量的大小

例如:上图中,类的大小为8字节

PS:内存对齐,本质上是牺牲空间换取效率。通过调整默认对齐数可以对这一过程进行动态调整。


二. 空类的大小

  • 没有成员变量的类对象,需要 1byte ,是为了占位,表示对象存在.

三.内部类

概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。

注意:内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中 的所有成员。但是外部类不是内部类的友元。

 特性:

  • 内部类可以定义在外部类的public、protected、private都是可以的。
  • 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
  • sizeof(外部类)=外部类,和内部类没有任何关系。

四.类的六个默认成员函数

特点:

  • 当没有显式定义(我们不主动写时),编译器会自动生成

1.构造函数(第一个)

  •  默认构造函数(3种):(1) 类自己生成的函数(2)无参 (3)全缺省的函数
  •  特征:  (不传参就可以调用)  

 构造函数的主要任务是初始化对象,如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义,编译器将不再生成。

  • 运作上看,当对象实例化时,编译器会自动调用
  • 形态上看,其名字与类名相同且无返回值
  • 注意点,构造函数允许重载

一.什么时候需要自己写构造函数? 

需要自己写的情况:

  •  一般情况下,有内置类型成员,要自己写(否则会初始化成随机值

不需要自己写的情况:

  • 内置类型成员都有缺省值时,且初始化符合要求,可以考虑让编译器自己生成
  • 全部都是自定义类型成员(例如:Stack),可以考虑让编译器自己生成

                             注意!!!


二.构造函数可以使用重载和不可以使用重载的情况

  •   构造函数可以用重载的情况:
typedef int DataType;
class Stack
{
public:
	Stack(DataType* a, int n)    //特定初始化
	{
		cout << "Stack(DataType* a, int n)" << endl;
		_array = (DataType*)malloc(sizeof(DataType) * n);
		if (NULL == _array)
		{
			perror("malloc申请空间失败!!!");
			return;
		}
		memcpy(_array, a, sizeof(DataType) * n);

		_capacity = n;
		_size = n;
	}
    //调用时可用以用d1,使用上方的构造函数
	Stack d1(int, 11); 
	//Stack d1(); // 不可以这样写,会跟函数声明有点冲突,编译器不好识别
	Stack d2;
    //调用时可以用d2,使用下方的构造函数

	Stack(int capacity = 4)    //构造函数(全缺省)
	{
		cout << "Stack(int capacity = 4)" << endl;
		_array = (DataType*)malloc(sizeof(DataType) * capacity);
		if (NULL == _array)
		{
			perror("malloc申请空间失败!!!");
			return;
		}

		_capacity = capacity;
		_size = 0;
	}
/*以下代码仅为完整性
void Push(DataType data)
	{
		CheckCapacity();
		_array[_size] = data;
		_size++;
	}

	void Pop()
	{
		if (Empty())
			return;
		_size--;
	}

	DataType Top() { return _array[_size - 1]; }
	int Empty() { return 0 == _size; }
	int Size() { return _size; }
~Stack()
	{
		cout << "~Stack()" << endl;
		if (_array)
		{
			free(_array);
			_array = NULL;
			_capacity = 0;
			_size = 0;
		}
	}

private:
	void CheckCapacity()
	{
		if (_size == _capacity)
		{
			int newcapacity = _capacity * 2;
			DataType* temp = (DataType*)realloc(_array, newcapacity * sizeof(DataType));
			if (temp == NULL)
			{
				perror("realloc申请空间失败!!!");
				return;
			}
			_array = temp;
			_capacity = newcapacity;
		}
	}*/
private:
	DataType* _array;
	int _capacity;
	int _size;
};
  • 构造函数不能用重载的情况:无参调用存在歧义
// 构成函数重载
	// 但是无参调用存在歧义
	  Date()
	{
		_year = 1;
		_month = 1;
		_day = 1;
	}

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

2.析构函数 (第二个)

析构函数的主要任务是清理对象;

  • 运作上看,当对象生命周期结束时,编译器会自动调用
  • 形态上看,其在类名前加上~且无返回值
  • 注意点,析构函数不允许重载

默认析构函数与默认构造函数类似,编译器对内置类型成员不做处理,对自定义类型会去调用它的析构函数。


一.什么时候需要自己写析构函数? 

需要自己写的情况:

  • 有动态申请资源时,需要自己写析构函数释放空间。(防止内存泄漏

不需要自己写的情况:

  • 没有动态申请资源时,不需要自己写,系统会自动回收空间。
  • 需要释放资源的对象都是自定义类型时,不需要自己写


3.拷贝构造函数  (第三个)

行为:

  • 在创建对象时,创建一个与已存在对象一模一样的新对象

拷贝构造函数:

  • 只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰)
  • 在用已存在的类类型对象创建新对象时由编译器自动调用(区分于构造函数)
  • 拷贝构造函数构造函数的一个重载形式
已知类Date,已经有实例化的对象 Date d1;
此时想得到一个和d1一模一样的对象d2;
Date d2(d1);
类中若有拷贝构造Date (const Date d);
直接进行调用;
d2传给没有显示的this指针,d1传给const Date d;
Date d2(const Date d1)
  • 拷贝构造函数的参数只有一个且必须是类类型对象引用
当拷贝构造函数为 Date(const Date &d);//引用
Date(const Date d);//错误写法

Date(const Date &d)
	{
		this->_year = d.year;
		this->_month =d.month;
		this->_day =d.day;
	}
//this 为d2的指针,d为拷贝的类d1
  • 原因:【使用传值方式编译器直接报错,因为会引发无穷递归调用】(错误方式) 

       


一.什么时候需要自己写拷贝构造函数?   

默认生成的拷贝构造函数为:浅拷贝

需要自己写的情况:

  • 自定义类型必须使用拷贝构造(深拷贝) 

不需要自己写的情况:

  • 内置类型直接拷贝(浅拷贝/值拷贝)

例:Date类中都是内置类型,默认生成的拷贝构造函数为浅拷贝可以直接用;

而Stack类为自定义类型,其中有a指针指向一块新开辟的空间。此时需要自己写拷贝构造函数。


二.默认拷贝构造(浅拷贝)的缺陷:

浅拷贝的缺陷(默认拷贝构造运用 引用 防止死递归的后遗症)


4.运算符重载函数(第四个)

运算符重载:

  • 参数类型:const T& (传递引用可以提高传参效率) 
  • 函数名:关键字operator后面接需要重载的运算符符号
  • 函数原型:返回值类型+operator操作符+(参数列表)

运算符重载 底层转化演示:

 注意:

  • 不能通过连接其他符号来创建新的操作符:例如operator@
  • 重载操作符必须有一个类型参数
  • 用于内置类型的运算符,其含义不能改变:例如+
  • 作为类成员函数重载时,其形参看起来比操作数少一个(因为成员函数的第一个参数为隐藏的this)
  • .* / :: /sizeof/ ?: /./这五个运算符不能重载 

一.运算符重载函数和构造函数使用区别:


5.赋值重载函数(第四个的分支)

赋值运算符重载格式:

  • 参数类型:const T& (传递引用可以提高传参效率) 
  • 返回值类型:T&  (返回引用可以提高返回的效率,有返回值的目的是为了支持连续赋值
  • 检测是否可以自己给自己赋值
  • 返回 *this:(对this指针解引用,要符合连续赋值的含义)

  • 赋值运算符只能重载成为类的成员函数而不能重载成全局函数(如果重载成全局函数,编译器会生成一个默认运算符重载)
  • 用户没有显示实现时,编译器会生成一个默认赋值运算符重载以值的方式(浅拷贝)逐字节拷贝。(注意点:内置类型成员变量直接赋值,只有自定义成员变量需要调用对应的赋值运算符重载


6.取地址与取地址重载(第五个&第六个)

引入: 内置类型取地址时有取地址操作符,而自定义类型呢?于是出现了取地址重载。它用到的场景非常少,可以说取地址重载——补充这个语言的完整性,更加系统。 

这两个默认成员函数一般不用重新定义 ,编译器默认会生成

  • 这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如不想让别人获取到指定的内容! (设为nullptr)

代码演示:

class Date
{ 
public :
 Date* operator&()
 {
 return this ;
// return nullptr;让普通成员的this指针不被取到
 }
 
 const Date* operator&()const
 {
 return this ;
 }
private :
 int _year ; // 年
 int _month ; // 月
 int _day ; // 日
};

五.初始化列表


一.初始化列表和构造函数的关系

引入:构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化, 构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为始化只能初始化一次,而构造函数体内可以多次赋值。


二.初始化列表基本结构 

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

代码展示: 

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. 类中包含以下成员,必须放在初始化列表位置进行初始化:
  • 引用成员变量
  • const成员变量
  • 自定义类型成员(且该类没有默认构造函数时 )

缺省值与初始化列表的关系: (下列代码中 int x 有演示)

  • 初始化列表没显式定义,缺省值给到初始化列表
  • 初始化列表显式定义,以初始化列表为主

代码展示: 

class A
{
  public:                内置类型可以放到初始化列表中初始化
    A(int a)
    :_a(a)
    {}
  private:
    int _a;
};

class B
{
  public:
    B(int a, int ref)      必须放到初始化列表中进行初始化
    :_aobj(a)
    ,_ref(ref)
    ,_n(10)
    {}

  private:
    A _aobj; // 没有默认构造函数  (无参/全缺省/默认生成)
    int& _ref; // 引用
    const int _n; // const 

    int x = 3;    缺省值为3,缺省值是给初始化列表的
                但是如果初始化列表中显式定义,则以初始化列表为主
};

四.尽量使用初始化列表初始化

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

五.成员变量在初始化列表中的初始化顺序

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

图示:

 

猜你喜欢

转载自blog.csdn.net/YYDsis/article/details/130901816