【入门C++编程的艺术】类和对象 | 类的六个默认成员函数 | 初始化列表 | 友元 | explicit关键字 | static成员

💛不要有太大压力🧡
💛生活不是选择而是热爱🧡

在这里插入图片描述

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

如果一个类中什么成员都没有,简称为空类。如class Date{};

空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数 (编译器做的事)

默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数

image-20220918181912182

1. 构造函数

概念

我们写一个数据结构或者写一个普通的类,我们通常会写一个Init函数来进行初始化,一般是把成员设置为nullptr或者0

缺陷:

  1. 有时候容易忘记调用Init函数初始化
  2. 频繁的自己调用,很麻烦

针对这一现象,能不能保证对象一定被初始化,并且不用我们调用呢?— 构造函数

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

注意:构造函数并不是开空间创建,而是定义对象的时候自动完成初始化,并且一定会初始化

特性

  1. 函数名与类名相同。
  2. 无返回值。
  3. 对象实例化时编译器自动调用对应的构造函数。
  4. 构造函数可以重载。

注意:

  • 无返回值不是void,void是空返回值,也是有返回值的!
  • 调用构造函数,如果不传参则调用默认构造,如Date d
class Date
{
public:
	// 1.无参构造函数
	Date()
	{}
	// 2.带参构造函数
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
    Date d1; // 调用无参构造函数
	Date d2(2015, 1, 1); // 调用带参的构造函数
    return 0;
}
  1. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成 ,所以如果用户显式定义的构造函数中没有默认构造,就会报错
  2. 默认构造函数指的是:可以不传参的构造函数。包括三种:
    1. 全缺省的
    2. 无参的
    3. 我们不写编译器默认生成的
  3. 一个类必须要有默认构造,否则就会报错,并且只能有一个默认构造
class Date
{
public:
	/*
	// 如果用户显式定义了构造函数,编译器将不再生成
	Date(int year, int month, int day)
	{
	_year = year;
	_month = month;
	_day = day;
	}

	*/
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	// 将Date类中构造函数屏蔽后,代码可以通过编译,因为编译器生成了一个无参的默认构造函数
	// 将Date类中构造函数放开,代码编译失败,因为一旦显式定义任何构造函数,编译器将不再生成
	// 下面调用默认构造函数,后报错:error C2512: “Date”: 没有合适的默认构造函数可用
	Date d1;	//调用默认构造函数
	return 0;
}

注意:

  • 编译器默认生成的无参默认构造函数,对内置类型(int,float,double等)不做处理,对于自定义类型(其他类)回去调用它的默认构造函数
class Time
{
public:
	//默认构造函数
	Time(int time = 0)
	{
		cout << "Time(int time = 0)" << endl;
		_hour = time;
	}
private:
	int _hour;
};
class Date
{
public:

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
    //内置类型
	int _year;
	int _month;
	int _day;
    //自定义类型
	Time _t;
};
int main()
{
	Date d1;
	return 0;
}

image-20220918191012300

image-20220918190948830

说明:Date调用编译器默认生成的无参默认构造,所以Date里的自定义类型不做处理。 Date中自定义成员_t调用Time类的默认构造,从而初始化为0

  1. C++11针对内置类型不初始化的缺陷打了补丁:内置类型可以在类中声明的时候给默认值(缺省值)从而完成初始化
class Date
{
private:
	// 给缺省值对内置类型初始化
	int _year = 1970;
	int _month = 1;
	int _day = 1;
	// 自定义类型
	Time _t;
};
  1. 语法上无参默认构造和全缺省默认构造可以同时存在,但是调用会存在歧义,报错
class Date
{
public:
	Date()//1、无参构造函数
	{
		_year = 2000;
		_mouth = 2;
		_day = 5;
	}
	Date(int year=2000, int mouth=2, int day=5)//2、带参构造函数
	{
		_year = year;
		_mouth = mouth;
		_day = day;
	}
 
private:
	int _year;
	int _mouth;
	int _day;
};
int main()
{
	Date d1;//error
    //无参调用,编译器不知道调用全缺省还是无参的构造函数,存在歧义(编译器报错)
	Date d2(2022, 2, 12);//correct
	return 0;
}

总结:

  • 对于一般的类,一般都不会让编译器默认生成构造函数,自己写!显式写一个全缺省默认构造即可

  • 对于一些特殊的类,类的成员都是自定义类型,我们不需要写构造函数,利用编译器自动生成的默认构造即可

2. 析构函数

概念

析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作

资源指的是:成员中可能存在动态开辟的空间的指针,如果不单独释放,会造成内存泄漏

如:Stack类中会存在int* _a的变量,指向在堆上动态开辟的内存空间

特性

析构函数是特殊的成员函数

  1. 析构函数名是在类名前加上字符 ~。
  2. 无参数无返回值类型。
  3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
  4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。
// 对于Stack类,需要自己写析构函数释放资源

class stack
{
public:
	stack(int capacity = 4)
	{
		_a = (int*)malloc(sizeof(int) * capacity);
		if (_a == nullptr)
		{
			cout << "malloc fail" << endl;
			exit(-1);
		}
	}
    //析构函数
	~stack()
	{
		free(_a);	//释放资源!
		_a = nullptr;
        _top = _capacity = 0;
	}
private:
	int* _a;
	size_t _top;
	size_t _capacity;
	
};
int main()
{
	stack s1;
	stack s2(10);
    //在return之前,会调用s1和s2的析构函数
	return 0;
}

image-20220918194201189

  1. 编译器自动生成的默认析构:

    • 对于内置类型不做处理

    • 对于内置类型自动调用它的析构函数

如下利用两个栈实现一个队列:成员变量都是Stack类型

class stack
{
public:
	stack(int capacity = 4)
	{
        cout<<"Stack(int capacity = 4)"<<endl;
    }
	~stack()
	{
        cout<<"~Stack()"<<endl;
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	int* _a;
	size_t _top;
	size_t _capacity;
	
};
 
//两个栈实现一个队列
class MyQueue
{
public:
	// 默认生成构造函数和析构函数会对自定义类型成员调用他的构造和析构
private:
	stack pushST;
	stack popST;
};
int main()
{
	MyQueue q;
	return 0;
}

输出结果:image-20220918194722033
析构的完整事件:
如果当对象生命周期结束,完整事件如下:
1、调用该类的析构函数,并执行该析构函数的函数体(如果自己定义了)
2、对于其非静态成员,按照类中声明顺序的相反顺序分别调用各自的析构函数(基本类型不做处理)
所以,如果类中存在其他自定义类型的成员,即使自己写了该类的析构函数,执行完函数体之后,还是会自动调用其他自定义成员的析构函数,并且如果有多个自定义类型,后声明的先析构。

注意:

  • 创建哪个类的对象则调用该类的构造函数,销毁哪个类的对象则调用该类的析构函数
  • 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类

3. 拷贝构造

概念

如果在创建对象的时候,想要利用一个已有的对象 创建一个与之一样的新对象,就要用到拷贝构造

拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用 。

注意:拷贝构造也是构造函数,是构造函数的重载。只不过该构造的参数比较特殊,是本类型对象的引用

特性

  1. 拷贝构造函数是构造函数的一个重载形式
  2. 拷贝构造函数的参数只有一个必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发
    无穷递归调用 ,必须用引用!
  3. 引用一般加const,是为了防止修改原来的对象
  4. 拷贝构造的写法有两种
    1. Date d(d1);
    2. Date d = d1; // 会转化成Date d(d1)

引发无穷递归时的场景:

class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	// Date(const Date& d) // 正确写法
	Date(const Date d) // 错误写法:编译报错,会引发无穷递归
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1; //构造d1
	Date d2(d1); //利用d1构造d2
	return 0;
}

image-20220918195718837

  1. 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成
    拷贝,这种拷贝叫做浅拷贝,或者值拷贝

    在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的

class Time
{
public:
	Time()
	{
		_hour = 1;
		_minute = 1;
		_second = 1;
	}
	Time(const Time& t)
	{
		_hour = t._hour;
		_minute = t._minute;
		_second = t._second;
		cout << "Time::Time(const Time&)" << endl;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
private:
	// 基本类型(内置类型)
	int _year = 1970;
	int _month = 1;
	int _day = 1;
	// 自定义类型
	Time _t;
};
int main()
{
	Date d1;
	// 用已经存在的d1拷贝构造d2,此处会调用Date类的拷贝构造函数
	// 但Date类并没有显式定义拷贝构造函数,则编译器会给Date类生成一个默认的拷贝构造函数
	Date d2(d1);
	return 0;
}

可以观察到,Date类没有写构造函数,d2调用了编译器自动生成的构造函数完成对d1的拷贝,并且会自动调用自定义成员_t的拷贝构造

image-20220918201853568

注意★

关于深浅拷贝

  • 对于如Date这样的类,对象中没有开辟空间,利用默认生成的即可,值拷贝就够用了
  • 对于Stack这样的类,里面有动态开辟的空间,如果只是浅拷贝(值拷贝),那么就会出现两个指针指向同一块空间,此时就会出现问题
    1. 一个对象修改会影响另一个对象
    2. 会析构两次,程序崩溃
//下面的程序就会崩溃!因为浅拷贝!

typedef int DataType;
class Stack
{
public:
	Stack(size_t capacity = 10)
	{
		_array = (DataType*)malloc(capacity * sizeof(DataType));
		if (nullptr == _array)
		{
			perror("malloc申请空间失败");
			return;
		}
		_size = 0;
		_capacity = capacity;
	}
	void Push(const DataType& data)
	{
		// CheckCapacity();
		_array[_size] = data;
		_size++;
	}
	~Stack()
	{
		if (_array)
		{
		free(_array);
		_array = nullptr;
		_capacity = 0;
		_size = 0;
		}
	}
private:
	DataType* _array;
	size_t _size;
	size_t _capacity;
};
int main()
{
	Stack s1;
	s1.Push(1);
	s1.Push(2);
	s1.Push(3);
	s1.Push(4);
	Stack s2(s1);
	return 0;
}

image-20220918201008627

ps:同一块内存释放两次为什么崩溃?(个人理解)

同一块内存已经释放了,也就是空间还给操作系统了,而归还后的这块空间可能被别的程序使用,而指针还是指向这块空间(野指针),所以再去free,就可能这段可能正在给别人使用的内存给释放,从而造成非法读写内存。而系统无法判断是否这段空间已经被从新分配了,所以一律视为报错

解决方法:

对于Stack这样有动态开辟空间的类,我们要自己实现深拷贝!

拷贝构造发生情景

  1. 使用已存在对象创建新对象

    Date d(d1);

  2. 函数参数类型为类类型对象

    //传递实参时会拷贝给实参,
    void Func(Date d){}
    Func(d1);
    //实际发生:Date d = d1;//d1为实参`
    
  3. 函数返回值类型为类类型对象

    Date Func()
    {
        /**/
        Date d;
        return d;
        //返回时 栈帧会销毁 d销毁
        //所以会产生临时对象 利用临时对象才能返回
    }
    

    类似这样:

    image-20220918203221249

//举个例子:

class Date
 {
 public:
	 Date(int year, int minute, int day)
	 {
		 cout << "Date(int,int,int):" << this << endl;
	 }
	 Date(const Date& d)
	 {
		 cout << "Date(const Date& d):" << this << endl;
	 }
	 ~Date()
	 {
		 cout << "~Date():" << this << endl;
	 }
 private:
	 int _year;
	 int _month;
	 int _day;
 };
 Date Test(Date d)
 {
	 Date temp(d);
	 return temp;
 }
 int main()
 {
	 Date d1(2022, 1, 13);
	 Test(d1);
	 return 0;
 }
    

image-20220918203522733

拷贝构造存在较大的消耗,所以为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用尽量使用引用

4. 赋值运算符重载

运算符重载

什么是运算符重载?

对于内置类型如int,我们有这样的情况

int a = 10;
int b = 20;
a+=10;
a+=b;
a==b;
a>b;
a-10;
a-b;
// ....
// ....

这些运算符是十分直观而且易理解的,而我们如果想让自定义类型也可以用其中特定的一部分,所以有了运算符重载

如,让Date类的对象d + 一个天数 d + 10

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似

赋值运算符重载:让自定义类型对象可以用运算符。转换成调用这个重载函数

区分函数重载:支持函数名相同的函数同时存在

特征:

  1. 函数名字为:关键字operator后面接需要重载的运算符符号

  2. 函数原型:返回值类型 operator操作符(参数列表) ,如:Date& operator +=(Date& d , int day)

  3. 参数:操作符有几个操作数,它就有几个参数

注意:

  1. 不能通过连接其他符号来创建新的操作符:比如operator
  2. 重载操作符必须有一个类类型参数, 如果都是自定义类型就不是重载了,对自定义类型库中都有对于函数
  3. 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
  4. 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
  5. .* :: sizeof ?: . 注意以上5个运算符不能重载。

运算符重载可以重载成全局,也可重载在类的内部

但是如果重载成全局涉及到一个问题:类成员的访问

  • 如果类成员是私有,需要设置友元
  • 如果是公有,虽然可以访问,但丧失了封装性

所以,一般赋值运算符重载都重载在特定的类内部!

例子:

  1. 实现全局的operator ==

    // 全局的operator==
     class Date
     {
     public:
    	 Date(int year = 1900, int month = 1, int day = 1)
    	 {
    		 _year = year;
    		 _month = month;
    		 _day = day;
    	 }
         //设置为public
    	 int _year;
    	 int _month;
    	 int _day;
     };
     bool operator==(const Date& d1, const Date& d2)
     {
    	 return d1._year == d2._year
    		 && d1._month == d2._month
    		 && d1._day == d2._day;
     }
     void Test()
     {
    	 Date d1(2018, 9, 26);
    	 Date d2(2018, 9, 27);
         //调用operator==
    	 cout << (d1 == d2) << endl;
         //实际转化为 cout << operator==(d1,d2)<<endl;
     }
    
  2. 类内部的operaotr==

     class Date
     {
     public:
    	 Date(int year = 1900, int month = 1, int day = 1)
    	 {
    		 _year = year;
    		 _month = month;
    		 _day = day;
    	 }
    	 // bool operator==(Date* this, const Date& d2)
    	 // 这里需要注意的是,左操作数是this,指向调用函数的对象
    	 bool operator==(const Date& d2)
    	 {
    		 return _year == d2._year 
    		 && _month == d2._month
    			 && _day == d2._day;
    	 }
     private:
    	 int _year;
    	 int _month;
    	 int _day;
     };
    void Test()
     {
    	 Date d1(2018, 9, 26);
    	 Date d2(2018, 9, 27);
    	 cout << (d1 == d2) << endl;
        //转化成cout << d1.operator==(d2) << endl;
     }
    

所以对于运算符重载,实际上就是编译器做了更多的事情,我们写运算符形式,编译器会自动转成调用其原来的样子:

image-20220918212356113

编译器会自动转换,我们不用管

赋值运算符的重载

赋值和拷贝构造的区别

如果定义一个对象的时候,用另一个对象去构造,那么就是拷贝构造

Date d(d1) 或 Date d = d1

但是如果一个对象已经存在(已经创建好了),再用另一个对象去赋值,这里就是单纯的赋值了

Date d;
d = d1;


赋值运算符的实现

  1. 参数类型:const T&,传递引用可以提高传参效率
  2. 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
  3. 检测是否自己给自己赋值
  4. 返回*this :要复合连续赋值的含义
 class Date
 {
 public:
	 Date(int year = 1900, 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;
	 }
     //赋值重载
	 Date& operator=(const Date& d)//第一个参数是this,我们看不到
	 {
         //如果自己给自己赋值,直接返回
		 if (this != &d)
		 {
			 _year = d._year;
			 _month = d._month;
			 _day = d._day;
		 }
		return *this;
	 }
 private:
	 int _year;
	 int _month;
	 int _day;
 };

image-20220918213753747

注意

  1. 赋值运算符只能重载成类的成员函数不能重载成全局函数

    原因:赋值运算符重载是类的默认成员函数,如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数

    C++就是这么设计的!如C++prime里所说

    image-20220918214114154

  2. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注意:内置类
    型成员变量是直接赋值的
    ,而自定义类型成员变量会去调用它的赋值运算符重载完成赋值

  3. 赋值运算符同样存在深浅拷贝问题,如果像Stack那样的类的对象要进行赋值,必须要自己实现深拷贝,否则会发生同一个空间析构两次导致崩溃

const成员

我们知道,每一个成员函数都有一个隐含的this指针

this指针不能改变,但是this指针指向的内容可以改变。

但是如果我们想用一个const对象调用成员函数的时候,就会出错

比如:

const Date d;
d.print();//error

image-20220918224948852

所以针对这一现象,必须要有const成员函数的存在!但是this指针又是对用户隐藏的,所以如何把this指针修饰为const呢?

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

const加在函数的后面:

void Display() const,如下图是实际的转换

image-20220918225250937

注意

const修饰成员函数的作用:

  1. 实际修饰this指向的内容,保证成员函数内部不会修改成员变量
  2. const对象和非const对象都可以调用该成员函数,非const对象调用就是权限的缩小,没问题

注意事项:(传参时权限的放大和缩小)

const对象可以调用非const成员函数

非const对象不可以调用const成员函数

const成员函数内部不能调用其他非const成员函数

非const成员函数内部可以调用其他const成员函数

总结:

除了在函数内部需要修改成员变量的成员函数,大部分成员函数应该给const,这样const对象和非const对象都可以调用

5&6. 取地址和const取地址操作符重载

取地址就是要返回对象的地址

对于一般的内置类型,我们可以直接使用

int a = 10;
cout << &a << endl;

对于自定义类型,有时也需要返回地址

class Date
 {
 public:
    //下面两个运算符重载构成函数重载(以参数this的类型区分)
    //普通对象调用
	 Date* operator&()
	 {
		 return this;
	 }
    //const对象调用
	 const Date* operator&()const
	 {
		 return this;
	 }
 private:
	 int _year; // 年
	 int _month; // 月
	 int _day; // 日
 };

但是这两个取地址函数是默认成员函数,我们不写会自动生成,并且这两个运算符一般不需要重载,使用编译器默认生成的取地址就可以了,只有很特殊的情况才会去重载,比如想让别人获取到指定的内容而不是直接返回真实的地址

比如:

class Date2
 {
 public:
	 string operator&()
	 {
		 return "想要我地址?没门!";
	 }
	 const Date2* operator&()const
	 {
		 return this;
	 }
 private:
	 int _year; // 年
	 int _month; // 月
	 int _day; // 日
 };
 int main()
 {
	 Date2 dd;
	 cout << &dd << endl;

	 return 0;
 }

image-20220918232700001


二、初始化列表

初始化列表的引入

再谈构造函数

在创建对象的时候,编译器通过调用构造函数,给对象的每一个成员变量一个初始值。那么它是什么时候给它的初始值呢?

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

此时在上面的构造函数中,其初始化是在函数体内初始化的
但实际上,不能将其称为对对象中成员变量的初始化,构造函数体中的语句只能将其称为赋初值

如果只有在函数体内赋初值,存在一些不好赋值的情况,如下面的代码

class Date
{
public:
	Date(int year, int hour)
	{
		_year = year;
		Time t(hour);//先构造
		_t = t;
		
	}
private:
	int _year;
	int _hour;
	Time _t;

};
class Time
{
public:
	//如果没有默认构造函数
	Time(int hour)
	{
		_hour = hour;
	}
private:
	int _hour;
};

上面的例子中,Time没有默认构造函数,在调用Date的构造函数的时候,
由于是在函数体内赋初始值,所以对于自定义类型Time,需要先构造出一个对象
然后在进行拷贝构造来给Time类型的成员变量赋初值.
显然十分麻烦

所以C++设计了一种初始化的方式初始化列表:

初始化列表定义和使用

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

C++规定,在初始化列表的时候对成员变量进行初始化
内置类型:如果不给显示初始化,默认是随机值(不做处理)

自定义类型:如果不给显示初始化,调用自定义类型的默认构造函数
如果给了显示初始化,那么就按照显式初始化的值

所以由该规定可知,上面的代码会出错!
因为Time没有默认成员函数,会出现找不到默认成员函数的错误

也就是说,上面的Date的构造函数在编译器的视角其实是这样的:

Date(int year, int hour)
	:_t()          //此处自动调用_t类型的默认构造函数
	{
		_year = year;
		Time t(hour);//先构造
		_t = t;	
	}

运行结果:
Pasted image 20220807111033

此时显示Time没有默认构造函数可用
是因为,在Date()之后有一个初始化列表,这里进行一次对象的初始化

  • 对于内置类型如果没显示给初始化,初始化为随机值
  • 对于自定义类型是调用自定义类型的默认构造去实现初始化

而Time没有默认构造,所以就会报错

之前所说的创建一个对象就会调用构造函数去初始化对象,初始化就是在这个地方进行初始化的

如果提供了默认构造,就在函数体直接对自定义类型进行初始化:

![[C++初始化列表.gif]]

可以看到,在进入函数体之前就对自定义类型进行了初始化(调用默认构造函数)
所以,利用初始化列表,不用在构造函数函数体内进行初始化了
在函数体之外就可以完成

  • 对于内置类型,在初始化列表和函数体内部进行初始化实际上差不多
  • 但对于自定义类型的成员变量,在初始化列表初始化是高效直接的,当然如果该自定义类型没有默认构造函数,必须显示使用初始化列表
/* 如果Time没有默认构造:*/*
Time(int hour)
{
	_hour=hour;
}
//此时要初始化_t成员,只能通过初始化列表
Date(int year,int hour)
	:_t(hour)
{
	_year=year;
}

/**********************************************************/

/* 如果Time有默认构造 */
Time(int hour = 0)
{
	_hour=hour;
}
//初始化_t成员,可以在函数体内赋值,但是还是会先走初始化列表调用默认构造
Date(int year,int hour)
{
	_year=year;
	Time t(hour);
	_t = t;
}


/** 既然如何都要调用初始化列表,最好的方式:自定义类型直接使用初始化列表 
(不管自定义类型有没有默认构造,都不会出错)**/

Date(int year,int hour)                Date(int year,int hour)
	:_t(hour)                              :_t(hour)
{,_year(year)
	_year = year;                      {}
}

总结 1

自定义类型成员推荐使用初始化列表初始化,内置类型成员无所谓
初始化列表可以认为是成员变量定义的地方

必须在初始化列表初始化的三种成员

除了无默认构造函数的自定义类型成员之外,还有两种类型的成员变量必须在初始化列表进行初始化

  1. 引用成员变量
  2. const成员变量

原因:因为const修饰的变量具有常性,只有一次初始化的机会
如果在初始化列表的位置不进行初始化,那么之后这个变量就一直是随机值了,不可以再改了,所以C++规定 对于const修饰的变量,必须在初始化列表的位置进行初始化
对于引用成员变量同理,引用只能在定义的时候初始化
int& ra = a
并且如果成员变量是引用类型,那么构造函数的传参对应就是一个引用
否则无法修改外部实参的值(此时一定是需要传递一个变量来修改的)
如:

class A
{
public:
//这里形参需要是引用,_c才能是main中实参的别名
	A(int a,int b,int& c)
		:_a(a)
		,_b(b)
		,_c(c)  
	{
		_c++;//实参x也++了
	}
private:
	int _a;
	int _b;
	int& _c;

}
int main()
{
	int x = 0;
	A aa(1,1,x);
	return 0;
}

所以,三种必须在初始化列表进行初始化的成员:

  1. 引用成员变量
  2. const成员变量
  3. 自定义类型成员(并且没有默认构造)

初始化列表的其他注意事项

1. 内置成员的缺省值
如果给内置成员变量一个缺省值,那么该缺省值是什么时候赋值给成员变量的呢?
没错,就是在初始化列表
在初始化列表没有显示给值的时候,缺省值就发挥作用了
Pasted image 20220807120920

所以,对于上面的必须在初始化列表进行初始化的成员,也可以在声明的时候给这些成员一个缺省值,这样在走初始化列表的时候,可以不用显式初始化,它会自动拿对应的缺省值去初始化!
如果不给缺省值,就必须显式初始化了

2. 不适合使用初始化列表的情景
如果我们需要动态开辟内存,那么初始化列表就不是很合适了

class A
{
public:
	//初始化一个数组,用初始化列表
	A(int N)
		:_a((int*)malloc(sizeof(int)*N))
		,_N(N)
	{
		if(_a==NULL)
		{
			perror("malloc fail");
		}
		memset(_a,0,sizeof(int)*N);
	}
/**************************************************************/
	//不使用初始化列表
	A(int N)
	{
		_a = (int*)malloc(sizeof(int)*N);
		if(_a==NULL)
		{
			perror("malloc fail");
		}
		memset(_a,0,sizeof(int)*N);
	}
private:
	int* _a;//数组 
	int _N;
}

所以说,这种情况用初始化列表就显得比较别扭了。
考虑到需要检查,开辟空间,还是在函数体内初始化比较好

3. 初始化列表的初始化顺序

初始化列表的初始化顺序,是根据成员变量的声明顺序来的,谁先声明的谁就先初始化

class A
{
public:
	A(int a)
		:_a1(a)
		,_a2(_a1)
	{}
	void Print(){
		cout<<_a1<<" "<<_a2<<endl;
	}
private:
//成员变量的声明
	int _a2;
	int _a1;
}


// 程序结果:
// 1    随机值
// 因为_a2先声明,_a2先初始化 , _a1后初始化

三、explicit关键字

构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用

class Date
{
public:
	Date(int year)
		:_year(year)
	{
		cout << " Date(int year)" << endl;
	}
private:
	int _year;
};

int main()
{
	Date d1(2022);//直接调用构造
	Date d2 = 2022;//存在隐式类型转换:构造 + 拷贝构造 +编译器优化 
					// ->相当于直接调用构造
	return 0;
}

对于只有一个参数的构造函数,或者只有第一个参数没有默认值的拷贝构造函数来说,其定义方式有两种

  1. 直接调用构造函数
  2. 可以利用赋值=来进行构造,这个过程中存在隐式类型的转换。以上面的代码为例:
    1. 首先会利用所给的值(2022)构造一个临时对象
    2. 然后把构造出来的临时对象拷贝构造给 d2
    3. 一般编译器会进行优化,直接优化成 直接调用构造

explicit关键字就可以阻止这种隐式类型的转换
当在Date前面加了explicit之后,就变成了这样:
Pasted image 20220807184942

而临时变量具有常性,构造出来的临时对象也有常属性
所以如果利用一个引用接受,必须加const
并且:如果用引用接收,引用的必须是隐式类型转换过程中产生的临时对象
因此用引用接收,编译器不会优化为 直接构造

int main()
{
	Date& d1 = 2022;// 编译报错
	const Date& d2 = 2022;// 编译成功
	return 0;
}

隐式类型转换的应用场景

如果有string类型,传参的时候利用隐式类型转换就会十分自然:

#include<string>

//string存在这样一个构造函数:
/* string(const char* str)
   {}                     */
   

//自定义类型传参一般给引用
 //引用要加const(防止临时对象赋值导致权限扩大 
void func(const string& str)
{        
	/**/
}
int main()
{
	string s1("hello");//构造string对象 方式1
	string s2 = "hello";//构造string对象 方式2 (隐式类型转换)

	func(s1);//调用方式1:传递已经创建好的对象
	func("goodboy");//调用方式2:利用隐式类型转换
	                 //(会产生临时对象,形参要用const)
}

四、匿名对象

匿名对象就是没有定义名字的对象,C++允许这种做法
特点: 匿名对象的生命周期只在定义的这一行

Date(2022);//匿名对象,生命周期只在这一行
			//这一行调用构造函数之后马上调用析构函数

匿名对象的用处
如果要调用某一个成员函数,但是只是为了调用函数而没有必要创建一个有名的对象,就可以利用匿名对象

class A
{
public:
	void func()
	{}
}

int main()
{
	A().func();//直接利用匿名对象创建函数
	return 0;
}

五、static

static定义

引入

有些时候需要变量在所有的变量中都用到,比如有一个类person
存在对于个体的名字年龄
也存在对于整体类的人类历史的成员变量
人类历史是一个对于整个类的变量,而不是对于一个对象
这个成员变量,就适合利用static来修饰

class Person {
public:
	void showInfo()
	{
		/***/
	}
private:
	char _name[20];
	int _age;
	static int _peoHistory;
}

概念
声明为static的类成员称为类的静态成员
用static修饰的成员变量,称之为静态成员变量;
用static修饰的成员函数,称之为静态成员函数

静态成员变量一定要在类外进行初始化,因为类中只是声明

特性

  1. 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区。生命周期是整个程序的运行期间
  2. 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
  3. 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
    这里的对象.并不是去对象中找,而是突破类域
  4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
  5. 静态成员也是类的成员,受public、protected、private 访问限定符的限制

这里要注意:
如果成员变量都是公开的public
普通成员函数可以访问普通成员变量 和 静态成员变量
静态成员函数只能访问静态成员变量,不可以访问普通成员变量

如果成员变量是私有的private
普通成员变量的访问需要借助 普通成员函数接口
静态成员变量的访问需要借助 普通成员函数接口或者静态成员函数接口

以下为例子:

class Person
{
public:
	void showInfo(){}
	static void PrintHistory(){}
private:
	int _age;
	char _name[20];
public:
	static int peoHistory;//静态成员变量
};
//初始化静态成员变量
int main()
{
	//定义一个人
	Person p1;
	p1.showInfo();//调用普通成员函数
	p1.PrintHistory();//调用静态成员函数 方式1
	Person::PrintHistory();//调用静态成员函数 方式2
	cout << p1.peoHistory << endl;//通过对象访问静态成员变量
	cout << Person::peoHistory << endl;//通过类作用限定符访问静态成员变量

	return 0;
}

static使用场景

场景1:实现一个类,计算程序中创建出了多少个类对象

这个场景下就需要一个不依赖于对象的变量来记录对象的个数
所以就需要static成员变量

分析:
类的创建无非两个方法:

  1. 构造方法
  2. 拷贝构造
    类的销毁就是 析构方法
    所以只要调用构造方法或拷贝构造,对象数量+1
    而调用析构方法,对象数量-1
class A
{
public:
	//构造
	A() { ++_scount; }
	//拷贝构造
	A(const A& t) { ++_scount; }
	//析构
	~A() { --_scount; }
	static int GetACount() 
	{ 
		return _scount;
	}
private:
	static int _scount;
};
//在类外面初始化
int A::_scount = 0;

int main()
{
	//输出类对象的数量
	cout << A::GetACount() << endl;
}

场景2:定义一个只能在栈上定义对象的类

定义一个对象可以在栈上、堆上、静态区上
如果要求你定义一个类,这个类的对象只能定义在栈上,如何实现呢?
因为定义对象一定会调用构造函数,所以如果构造函数是开放的(可以让我们随便用的)
那么该对象就可以定义在栈上、堆上、静态区都可以
但是如果把构造函数设置为私有,而只提供一个创建对象的接口函数(静态的)
因为静态函数不依赖于对象就可以调用,而函数一定会开辟栈帧,这样在函数中创建的对象一定就是在栈上定义的对象,然后对象作为返回值来返回。


class StackOnly
{
public:
	//提供静态函数接口,来创建对象
	static StackOnly CreateObj()
	{
		StackOnly so;
		return so;//作为返回值的对象,一定是定义在栈上的
	}
	//把构造函数设置为私有
private:
	StackOnly(int x = 0, int y = 0)
		:_x(x)
		, _y(y)
	{}
private:
	int _x;
	int _y;
};
int main()
{
//利用接口来创建的对象,一定在栈上
	StackOnly so = StackOnly::CreateObj();
	return 0;
}

六、友元

有时候我们在类的外面无法访问类成员,通过友元就突破这种封装,有时候会提供遍历

友元函数

友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字

之前遇到的,如果想要重载operator <<,也就是cout运算符
因为在类中定义的成员函数第一个参数一定是this(默认的),我们无法更改
但是要实现的cout,cout才是第一个参数才符合使用逻辑:
cout << x即x流入cout,如果是x << cout显然用起来很别扭,不符合常规调用
所以operator<<函数必须在全局实现,不可以在类中
但是在全局又存在一个弊端,我们无法访问到类成员变量

class Date
{
public:
	Date(int year, int month, int day)
		: _year(year)
		, _month(month)
		, _day(day)
	{}
	// d1 << cout; -> d1.operator<<(&d1, cout); 不符合常规调用
	// 因为成员函数第一个参数一定是隐藏的this,所以d1必须放在<<的左侧
	ostream& operator<<(ostream& _cout)
	{
		_cout << _year << "-" << _month << "-" << _day << endl;
		return _cout;
	}
private:
	int _year;
	int _month;
	int _day;
};

所以,这里利用友元可以解决

class Date
{
	//声明该函数是这个类的友元函数(理解成朋友)
	friend ostream& operator<<(ostream& _cout, const Date& d);
public:
	Date(int year = 1900, int month = 1, int day = 1)
		: _year(year)
		, _month(month)
		, _day(day)
	{}
private:
	int _year;
	int _month;
	int _day;
};
//在类外面定义 << 的重载
ostream& operator<<(ostream& _cout, const Date& d)
{
	_cout << d._year << "-" << d._month << "-" << d._day;
	return _cout;
}

int main()
{
	Date d;
	cout<<d<<endl;//此时cout就是第一个参数,d就是第二个参数
}

注意事项

  • 友元函数可访问类的私有和保护成员,但不是类的成员函数
  • 友元函数不能用const修饰,因为库中的cout是ostream&类型,我们重载的cout的返回值不可以是const ostream&类型。简单来说就是参数就会不匹配
  • 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
  • 一个函数可以是多个类的友元函数,只需要在不同的类中都进行声明即可
  • 友元函数的调用与普通函数的调用原理相同

友元类

如果类A中要经常访问类B中的成员,那么直接可以把类A设置成类B的友元类
(对B来说,A是B的朋友,可以随便访问)

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

class Time
{
	friend class Date;
public:
	Time(int hour = 0, int minute = 0, int second = 0)
		: _hour(hour)
		, _minute(minute)
		, _second(second)
	{}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
		: _year(year)
		, _month(month)
		, _day(day)
	{}
	void SetTimeOfDate(int hour, int minute, int second)
	{
		// 直接访问时间类私有的成员变量
		_t._hour = hour;
		_t._minute = minute;
		_t._second = second;
	}
private:
	int _year;
	int _month;
	int _day;
	Time _t;
};

注意事项

  • 友元关系是单向的,不具有交换性。

    比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行

  • 友元关系不能传递

    如果B是A的友元,C是B的友元,则不能说明C时A的友元。

  • 友元关系不能继承(后面解释)。

内部类

把一个类定义在另一类的内部就是内部类。
内部类是一个是一个独立的类,不要认为定义在外部类的内部它就属于外部类。
所以不能通过外部类的对象去访问内部类的成员。
内部类和外部类就相当于两个不同的类
有区别的就是:

  1. 内部类的访问收到了外部类的限制,需要访问限定符
  2. 外部类默认是内部类的友元类(友元是单向,内部类不是外部类的友元)
/* Inner类定义在Outer的内部
   Inner是Outer的友元 */
   
class Outer
{
private:
	int _x;
	static int _z;
public:
	//内部类
	//内部类是外部类的友元
	class Inner
	{
	public:
		void PrintOuter(const Outer& a)
		{
			//内部类是外部类的友元,可访问外部类的私有成员
			
			cout << a._x << endl//普通成员用对象访问
			cout << _z << endl;//静态成员可以直接访问
		}
	private:
		int _y;
	};
};
int main()
{
	cout << sizeof(Outer) << endl;
	Outer obj1;
	Outer::Inner obj2;//定义inner类型
	return 0;
}

特性

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

✨感谢阅读~ ✨
❤️码字不易,给个赞吧~❤️

在这里插入图片描述

  • 32
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值