【C++】类和对象

C++类和对象

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

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

面向对象的三大特性:封装,继承,多态。

C++为什么要引入类

​ 在C语言中,为了表示变量的集合,我们使用了结构体,但是在C语言结构体中,只能定义变量,所以在C++中,我们对结构体进行了扩展,C++的结构体中可以定义函数.而且C++里面通常用class来代替struck来表示一个类.

1.类的基本概念

1.1 类的定义

class ClassName {
    //类体,由成员函数和成员变量组成.
};	//和结构体一样,后面要加";".

其中,class是定义类的关键字,ClassName为类名,{}中是类的主体,类结束要加";".

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

类有两种定义方式:

  • 一种是声明和定义全都放在类体中,成员函数如果在类中定义,编译器可能会将其作为内联函数处理.
class Person {
public:
    //成员函数在类内定义
	void show() {
		cout << "名字" << _name << endl << "性别" << _sex << endl << "年龄" << _age << endl;
	}
private:
	string _name;
	string _sex;
	int _age;
};
  • 一种是声明放在类内,类的实现放在类外,
class Person {
public:
	void show();
private:
	string _name;
	string _sex;
	int _age;
};
//成员函数的实现放在类外
void Person::show() {
	cout << "名字" << _name << endl << "性别" << _sex << endl << "年龄" << _age << endl;
}

要是成员函数的实现在类外,那成员函数名的前面要加上"::"(作用域解析符)用来说明函数属于哪个类.


1.2 类的访问限定符

访问限定符有三种,把类内的成员函数和成员变量赋予不同的访问权限,供用户合理调用.

  • private 私有
  • public 共有
  • protected 保护

访问限定符说明:

  • public修饰的成员在类外可以直接被访问

  • protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)

  • 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止

  • class的默认访问权限为private,struct为public(因为struct要兼容C)

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


C++中struct和class有什么区别?

C++需要兼容C语言,所以C++中struct可以当成结构体去使用。另外C++中struct还可以用来定义类,和class是定义类是一样的.

区别是struct的成员默认访问方式是public,class是struct的成员默认访问方式是private。


1.3 封装

封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行
交互。

​ 封装的本质就好比一种管理,比如对旅游景区的管理,如果什么都不管,任由游客随意游玩,那么景点肯定会遭到随意破坏,所以我们就把景点封装起来,然后再开放售票通道,安排了管理人员,这样游客游玩才能保证景点不被随意破坏。

​ 类也一样,我们使用类把数据和方法封装一下,不让别人随意访问的,我们使用private/protected把成员封装起来,开放一些共有的成员函数对成员合理访问,所以说封装的本质是一种管理。


1.4 类的作用域

类定义了一个新的作用域,类的所有成员都在类的作用域中。在类外定义的成员,需要使用"::"作用域解析符指明成员属于哪个类域。

class Person {
public:
	void show();
private:
	string _name;
	string _sex;
	int _age;
};
//成员函数的实现放在类外,使用作用域解析符指明类域
void Person::show() {
	cout << "名字" << _name << endl << "性别" << _sex << endl << "年龄" << _age << endl;
}

1.5 类的实例化

用类类型创建对象的过程,称为类的实例化。

class Dog {
private:
	string _DogName;
	string _DogKind;
public:
	void show() {
		cout << _DogName << "喜欢吃鸡肉" << endl;
	}
};
//
int main() {
    //这个是int类型的实例化,相当于定义了一个int变量count
	int count;
    //这个是类类型的实例化,相当于定义了一个Dog类对象DaHuang。DaHuang是一个Dog。
	Dog DaHuang;
	return 0;
}
  • 类只是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它
  • 一个类可以实例化出多个对象实例化出的对象 占用实际的物理空间,存储类成员变量

1.6 类对象的存储方式

​ 我们实例化了一个类对象,这个类对象的大小是由什么决定的,是成员变量的多少和大小,还是成员函数的数量来决定的。

//类中有成员变量和成员方法
class A1 {
public:
	void f1() {};
private:
	int _a;
};
//类中仅有成员方法
class A2 {
public:
	void f2() {}
};
//空类
class A3
{};
int main() {
	cout << "A1 Size:" << sizeof(A1) << endl;
	cout << "A2 Size:" << sizeof(A2) << endl;
	cout << "A3 Size:" << sizeof(A3) << endl;
	return 0;
}

请添加图片描述

可以看到结果,拥有成员变量的方法A1,大小为4,而int类型的大小刚好为4。而A2虽然拥有成员方法,但是没有成员变量,大小和空类A3的大小一样。

说明类对象被创建出来,只有成员变量占据空间,而成员方法和类本身是不占空间的,而空类和只拥有成员方法的类有一个字节的空间,这是为了保证每个实例化在内存中都有独一无二的地址,编译器会给一个空类或者空的结构体中加入一个字节,这样空类或空结构体在实例化后在内存中就得到了独一无二的地址。

我们要看类对象的存储方式,如果对象的存储方式是这样:

请添加图片描述

不同的对象有不同的变量和方法存储地址,很合理,访问也很方便,但是,我们所实例化的不同的类,也就是不同的对象,他们的区别,基本上就是成员变量之间的区别,他们的成员方法都是一样的,如果每一个类都存储一份成员方法,那会造成空间的浪费,所以类的实际存储方式是这样的:

请添加图片描述

对象中只存储成员变量,而每个类都相同的成员方法则被存在一个公共的代码段内,被每个对象调用,节省空间。

结论:一个类的大小,实际就是该类中”成员变量”之和,当然也要进行内存对齐,注意空类的大小,空类比
较特殊,编译器给了空类一个字节来唯一标识这个类。

结构体内存对齐规则:

  1. 第一个成员在与结构体偏移量为0的地址处。
  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
    注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
    VS中默认的对齐数为8
  3. 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
  4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是
    所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

1.7 this指针

刚写的Dog类

class Dog {
private:
	string _DogName;
	string _DogKind;
public:
	void setDog(string DogName, string DogKind) {
		_DogName = DogName;
		_DogKind = DogKind;
	}
	void show() {
		cout << _DogName << "是一个" <<_DogKind<< endl;
	}
};
int main() {
	Dog DaHuang, XiaoHei;
	DaHuang.setDog("大黄","金毛");
	XiaoHei.setDog("小黑", "中华田园犬");
	return 0;
}

​ 我用DaHuang.setDog();方法来设置DaHuang对象的成员变量,但是我们知道,不管实例化的对象是谁,他们都是公用同一份成员方法的代码,都用的是setDog()方法,那就是说,我用XiaoHei对象调用setDog()方法,也是用的同一份代码,但是setDog方法中的形式参数并没有传进去可以区分两个对象的参数,我们是如何用相同的代码给不同的对象设置变量的值呢?

C++中通过引入this指针解决该问题,即:C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参
数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有成员变量的操作,都是通过该
指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。

所以set对象实际应该是以下形式:

void setDog(Dog *const this,string DogName, string DogKind) {
		this->_DogName = DogName;
		this->_DogKind = DogKind;
	}

但是这些都是隐藏的,都是编译器帮我们完成传参的,用户不需要定义this指针,但是用户可以使用this指针。

由于this指针是指向当前对象的,所以我们不希望this指针被修改,所以this指针是一个常指针,不能被修改。

this指针的特性:

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

问题:

this指针存在哪里?

调试Dog类,转到反汇编

请添加图片描述

可以看到在实例化对象时,this指针被存在寄存器ecx当中(vs2019环境),也就是成员函数的其它参数正常都是存放在栈中。而this指针参数则是存放在寄存器中。类的静态成员函数因为没有this指针这个参数,所以类的静态成员函数也就无法调用类的非静态成员变量。

this指针可以为空吗?

#include<iostream>
using namespace std;
class Dog {
private:
	string _DogName;
	string _DogKind;
public:
	void setDog(string DogName, string DogKind) {
		_DogName = DogName;
		_DogKind = DogKind;
	}
	void show() {
		cout << _DogName << "是一个" <<_DogKind<< endl;
	}
	void showCall() {
		cout << "汪汪汪" << endl;
	}
};

int main() {
    //给D对象的this指针赋空
	Dog* D = nullptr;
	D->showCall();
	D->show();
	return 0;
}

调试这段代码,程序在调用show方法时发生了异常,进行了空指针访问

请添加图片描述

请添加图片描述

在调用showCall方法时,程序并没有奔溃,也就是说

this指针可以为空,当我们在调用函数的时候,如果函数内部并不需要使用到this,也就是不需要通过this指向当前对象并对其进行操作时才可以为空(当我们在其中什么都不放或者在里面随便打印一个字符串),如果调用的函数需要指向当前对象,并进行操作,则会发生错误(空指针引用)就跟C中一样不能进行空指针的引用。

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

如果一个类中什么成员也没有,简称为空类,那么它会自动生成六个默认的成员函数.

请添加图片描述

2.1 构造函数

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

我们看以下Dog类:

class Dog {
private:
	string _DogName;
	string _DogKind;
public:
    //对Dog信息进行初始化
	void SetDog(string DogName,string DogKind) {
		_DogName = DogName;
		_DogKind = DogKind;
	}
	void show() {
		cout << _DogName << "是一条" << _DogKind << endl;
	}
};

int main() {
	Dog wangcai;
	wangcai.SetDog("旺财","板凳狗");
	wangcai.show();
	return 0;
}

​ 我们要对Dog对象的信息进行初始化,就要自己实现一个初始化的函数SetDog,实例化完以后才能进行初始化,而且每创建一个对象就要调用一次初始化方法,所以C++中就有了构造函数,可以在对象被创建时就对其进行初始化.

class Dog {
private:
	string _DogName;
	string _DogKind;
public:
    //构造函数会在对象创建时自动调用,完成对象的初始化操作.
	Dog(string DogName, string DogKind) {
		_DogName = DogName;
		_DogKind = DogKind;
	}
	void show() {
		cout << _DogName << "是一条" << _DogKind << endl;
	}
};

int main() {
    //直接在创建对象时就传入要初始化的数据,对象创建时,成员变量就会被初始化
	Dog wangcai("旺财","板凳狗");
	wangcai.show();
	return 0;
}

构造函数的特征:

  • 函数名与类名相同
  • 无返回值
  • 对象实例化时编译器自动调用对应构造函数
  • 构造函数可以进行重载
class Dog {
private:
	string _DogName;
	string _DogKind;
public:
	//无参构造函数
	Dog(){}
	//带参构造函数
	Dog(string DogName, string DogKind) {
		_DogName = DogName;
		_DogKind = DogKind;
	}
	void show() {
		cout << _DogName << "是一条" << _DogKind << endl;
	}
};

int main() {
	Dog d1;
	//可以通过调用不同的构造函数对不同的对象进行初始化
	Dog d2("修勾", "小黑狗");
	d2.show();
	//注意如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
	Dog d3();//这个d3是一个函数声明,声明了一个返回值为Dog类型的无参函数.
	return 0;
}
  • 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定

    义编译器将不再生成.

    class Dog {
    private:
    	string _DogName;
    	string _DogKind;
    public:
    	无参构造函数
    	//Dog() {}
    	带参构造函数
    	//Dog(string DogName, string DogKind) {
    	//	_DogName = DogName;
    	//	_DogKind = DogKind;
    	//}
    	void show() {
    		cout << _DogName << "是一条" << _DogKind << endl;
    	}
    };
    
    int main() {
        //如果我们没有实现构造函数,对象也可以被创建,此时编译器调用的就是自动生成的无参构造函数
    	Dog d1;
    	d1.show();
    	return 0;
    }
    

    请添加图片描述

请添加图片描述

可以看出D1对象被创建,但是他的成员变量都是空值.

  • 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认成员函数。
class Dog {
private:
	string _DogName;
	string _DogKind;
public:
	//无参构造函数
	Dog() {}
	//带有全缺省参数的构造函数
	Dog(string DogName="佩奇", string DogKind="哈士奇") {
		_DogName = DogName;
		_DogKind = DogKind;
	}
	void show() {
		cout << _DogName << "是一条" << _DogKind << endl;
	}
};

int main() {
	Dog d1;
	d1.show();
	return 0;
}
//此时两个构造函数都是默认构造函数,在创建对象时,编译器无法分辨要调用哪个构造函数,所以无法编译通过.

请添加图片描述


2.2 析构函数

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

typedef int DateType;
class SeqList {
public:
	SeqList(size_t capacity){
		this->capacity = capacity;
		size = 0;
		_pDate = (DateType*)malloc(sizeof(DateType) * capacity);
	}
	~SeqList(){
		free(_pDate);
		_pDate = nullptr;
		capacity = size = 0;
	}
private:
	int* _pDate;
	size_t size;
	size_t capacity;
};

以顺序表为例,构造函数完成堆上空间的申请,析构函数完成对申请空间的清理工作。

析构函数特征:

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

析构函数的调用顺序:

构造函数和析构函数的调用顺序刚好相反,对象先创建,则析构就后调用(不包括静态对象和全局对象)。

class A {
public:
	A() {
		cout << "A被创建:" << this << endl;
	}
	~A() {
		cout << "A被析构:" << this << endl;
	}
};
class B {
public:
	B() {
		cout << "B被创建:" << this << endl;
	}
	~B() {
		cout << "B被析构:" << this << endl;
	}
};
class C {
public:
	C() {
		cout << "C被创建:" << this << endl;
	}
	~C() {
		cout << "C被析构:" << this << endl;
	}
};
class D {
public:
	D() {
		cout << "D被创建:" << this << endl;
	}
	~D() {
		cout << "D被析构:" << this << endl;
	}
};
int main() {
	A a;
	B b;
	C c;
	D d;
	return 0;
}

用这四个对象来看,a b c d 对象先后被创建,当对象的生命周期结束时,他们的析构顺序相反

请添加图片描述

但是当静态对象和全局对象被创建时,这个规则就会发生变化。

还是用上面的四个类,我们把 c 和 d 对象定义为静态和全局对象

请添加图片描述

说明静态对象的生成作用域长于局部对象,所以静态的c对象后于a b 局部变量,d对象先于c对象创建,所以c先于d析构。


2.3 拷贝构造函数

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

class Dog {
private:
	string _DogName;
	string _DogKind;

public:
	Dog(string DogName,string DogKind) {
		_DogName = DogName;
		_DogKind = DogKind;
	}
    //
	Dog(const Dog& d) {
		_DogName = d._DogName;
		_DogKind = d._DogKind;
	}
	void show() {
		cout << _DogName << "是一只" << _DogKind << endl;
	}
	~Dog(){
		cout << "Dog被析构" << this << endl;
	}
};

int main() {
	Dog d1("小黑","边牧");
	Dog d2 = d1;   //用对象给新对象赋值,就会调用拷贝构造函数。
	d1.show();
	d2.show();
	return 0;
}

请添加图片描述

可以看到,拷贝构造函数是构造函数的一个重载形式,拷贝构造的形式参数是一个常引用类型的参数,之所以传引用类型,是因为要是传的是一个对象,那么传递对象时要生成临时的对象给当前对象初始化,而创建临时对象进行初始化时也会调用拷贝构造函数,会造成无穷递归调用,所以在传参时,一定要传对象的引用。

class Dog {
private:
	string _DogName;
	string _DogKind;

public:
	Dog(string DogName,string DogKind) {
		_DogName = DogName;
		_DogKind = DogKind;
	}
	/*Dog(const Dog& d) {
		_DogName = d._DogName;
		_DogKind = d._DogKind;
	}*/
	void show() {
		cout << _DogName << "是一只" << _DogKind << endl;
	}
	~Dog(){
		cout << "Dog被析构" << this << endl;
	}
};

int main() {
	Dog d1("小黑","边牧");
    //没有实现拷贝构造函数,依旧可以进行赋值
	Dog d2 = d1;
	d1.show();
	d2.show();
	return 0;
}

请添加图片描述

若未显示定义,系统生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷
贝,这种拷贝我们叫做浅拷贝,或者值拷贝

而浅拷贝在遇到拷贝指针等问题时就会发生问题,这时候就要用深拷贝来解决。


2.4 赋值运算符重载

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

class Dog {
private:
	string _DogName;
	string _DogKind;
public:
	Dog(string DogName="佩奇", string DogKind="哈士奇") {
		_DogName = DogName;
		_DogKind = DogKind;
	}
    //=运算符的重载,调用这个函数,就会完成传入的对象参数对本对象的赋值操作
	Dog& operator=(const Dog& d) {
		if (this != &d) {
			_DogName = d._DogName;
			_DogKind = d._DogKind;
		}
		return *this;
	}
	void show() {
		cout << _DogName << "是一条" << _DogKind << endl;
	}
};

int main() {
	Dog d1("旺财", "柯基");
	Dog d2;
    //赋值操作
	d2 = d1;
	d2.show();
	return 0;
}

请添加图片描述

看似是d1=d2,运算符操作,其实本质上是函数的调用,是d2调用operator=函数,传入d1的引用对象,完成的赋值操作.

d2.operator=(d1);
//也可以这样操作完成赋值,但是用=更加好理解
Dog& operator=(const Dog& d) {
		if (this != &d) {
			_DogName = d._DogName;
			_DogKind = d._DogKind;
		}
		return *this;
	}

​ 来看这个函数,形参之所以传引用,是因为如果直接传对象,那么在传递过程中会形成临时对象,用临时对象给当前对象赋值,但是临时对象的构造又会调用拷贝构造函数,会增加开销,所以传引用过去会节约资源,提高效率.

之所以传const引用,是为了保证d1对象在对d2进行赋值时,不会对d1对象进行修改.

而有返回值是为了在对象进行连等赋值时,后面两个对象完成赋值后,返回值可以对前面的对象接着进行赋值,要是没有返回值,连等赋值就不能实现,之所以返回值为引用,是返回当前对象,返回值在函数周期结束后不会销毁,用引用返回可以减少一次无名对象的创建,也是提高效率.

一个类如果没有显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝(也就是浅拷贝)。


2.5 取地址操作符重载

只需返回当前对象的指针,也就是this指针即可

Dog* operator&() {
	return this;
}

2.6 const修饰的取地址操作符重载

const Dog* operator&()const {
	return this;
}
const成员

这里我们需要了解一下const成员函数

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

void show()const {
	cout << _DogName << "是一条" << _DogKind << endl;
}

比如这个成员函数,用const修饰,则它实际的意思是对this指针进行修饰,让传入这个函数的对象不能被修改.

void show(const Dog* this){
	cout << _DogName << "是一条" << _DogKind << endl;
}
//实际当中并不能这么写,因为this指针不能用户自己传递.

这里有对const成员的几个问题:

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

请添加图片描述

不能,可以看出const对象并不能调用非const成员函数,因为const对象的this指针被被修饰为const指针,而将const指针传入非const方法,const对象可能会被修改,所以const对象不能调用非const方法.

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

可以,非const对象可以进行修改,调用const成员函数,函数内部只对对象进行只读操作,所以并不会影响对象的权限.

也就是相当于将一个可修改的对象进行读取,是被允许的.

  • const成员函数内可以调用其它的非const成员函数吗?

不能,和第一个问题类似,const成员内部的对象是不能被修改的,而在内部调用非const成员函数,非const成员函数内部可以对对象进行修改,这也就违背了const成员函数的定义,所以不被允许.

  • 非const成员函数内可以调用其它的const成员函数吗?

可以,和第二个问题类似,也是权限缩小,并不会影响const成员函数的const属性,所以是可以调用的.

可以看出,const成员函数内部不能对对象进行修改,所以一些只进行读取操作的函数一般被修饰为const成员函数.

3.类和对象的补充

3.1 构造函数和初始化

​ 对象在创建时,编译器调用构造函数给对象的各个成员赋一个初值,但这个行为严格意义来讲不能称为初始化,构造函数体中的语句只能将其称作为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值.

class Dog {
private:
	string _DogName;
	string _DogKind;
public:
	Dog(string DogName1="佩奇", string DogName2="小黑",string DogKind = "哈士奇") {
		_DogName = DogName1;
		_DogName = DogName2;
        //对_DogName进行两次赋值,这个行为不能称之为初始化.
		_DogKind = DogKind;
	}
};

3.2 初始化列表

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

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成员变量

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

    class A {
    public:
    	A(int a)
    		:_a(a)
    	{}
    private:
    	int _a;
    };
    class B {
    public:
    	B(int a, int ref)
    		:_aa(a)
    		, _ref(ref)
    		, _n(10)
    	{}
    private:
    	A _aa; // 没有默认构造函数
    	int& _ref; // 引用
    	const int _n; // const 
    };
    

    ​ 这种初始化方式就相当于把_aa对象实例化的过程移到了B类的初始化列表初始化过程中,B类去实例化它的_aa成员时,发现A类并没有默认构造函数,而且_aa也没有传入参数,这时就跳转到B类的初始化列表中对_aa进行构造.

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

class A {
public:
	A(int a)
		:_a1(a)
		, _a2(_a1)
	{}

	void Print() {
		cout << _a1 << " " << _a2 << endl;
	}
private:
	int _a2;
	int _a1;
};
int main() {
	A aa(1);
	aa.Print();
}

请添加图片描述
请添加图片描述

可以看到初始化列表初始化和构造函数内部的构造语句不同,初始化列表初始化的顺序是成员变量的声明顺序,与其在初始化列表中的先后次序无关.

3.3 explicit关键字

构造函数不仅可以构造与初始化对象,对于单个参数的构造函数,还具有类型转换的作用

class Date
{
public:
	Date(int year)
		:_year(year)
	{}
	explicit Date(int year)
		:_year(year)
	{}
private:
	int _year;
	int _month;
	int _day;
};
void TestDate()
{
	Date d1(2018);
	// 用一个整形变量给日期类型对象赋值
	// 实际编译器背后会用2019构造一个无名对象,最后用无名对象给d1对象进行赋值
	d1 = 2019;
}

这时为了杜绝发生这种隐式转换,就会给构造函数前面加上explicit关键字来强制编译器不进行隐式转换

3.4 static成员

概念:

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

class A {
public:
	A() { ++_scount; }
	A(const A& t) { ++_scount; }
	static int GetACount() { return _scount; }
private:
	static int _scount;
};
//_scount是静态变量,只能在类内声明,类外定义.
int A::_scount = 0;
void TestA(){
    //A的静态成员方法不用创建对象,可以直接通过类名调用
	cout << A::GetACount() << endl;
	A a1, a2;
	A a3(a1);
	cout << A::GetACount() << endl;
}

特性:

  • 静态成员为所有类对象所共享,不属于某个具体的实例
  • 静态成员变量必须在类外定义,定义时不添加static关键字
  • 类静态成员即可用类名::静态成员或者对象.静态成员来访问
  • 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
  • 静态成员和类的普通成员一样,也有public、protected、private3种访问级别,也可以具有返回值

【问题】

静态成员函数可以调用非静态成员函数吗?

静态成员函数由于不能传递this指针,所以里面不能调用需要this指针的非静态成员函数.

非静态成员函数可以调用类的静态成员函数吗?

费静态成员内部满足调用静态成员函数的条件,可以调用.


C++11的成员初始化新玩法

非静态的成员变量,可以在声明时给变量缺省值

class A {
private:
    //非静态声明时给变量缺省值
	int a = 666;
};

3.5 友元

3.5.1 友元函数

现在我们尝试去重载operator<<,然后发现我们没办法将operator<<重载成成员函数。因为cout输出流对象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作数了。但是实际使用中cout需要是第一个形参对象,才能正常使用。所以我们要将operator<<重载成全局函数。但是这样的话,又会导致类外没办法访问成员,那么这里就需要友元来解决。operator>>同理。

class Date
{
public:
	Date(int year, int month, int day)
		: _year(year)
		, _month(month)
		, _day(day)
	{}
    //如果是成员函数,第一个参数就会是隐藏的this指针,但是我们在使用cout的时候,需要cout是第一个参数,不然不能正常使用
	ostream& operator<<(ostream& _cout)
	{
		_cout << d._year << "-" << d._month << "-" << d._day;
		return _cout;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main(){
	Date d(2017, 12, 24);
    //只能用这样的方式使用cout输出,不符合我们的使用习惯.
	d << cout;
	return 0;
}

把对<<的重载声明为友元函数,就可以在类外实现非成员函数访问成员变量的行为.

class Date
{
public:
    //友元函数在类内声明为友元,类外进行实现
	friend ostream& operator<<(ostream& _cout,const Date& d);
	Date(int year, int month, int day)
		: _year(year)
		, _month(month)
		, _day(day)
	{}
private:
	int _year;
	int _month;
	int _day;
};
//第一个参数为输出流指针对象,第二个参数传入d对象的引用,就能在类外实现非成员函数对私有成员变量的访问
//返回值也是输出流指针对象,就可以实现连续输出操作.
ostream& operator<<(ostream& _cout,const Date& d)
{
	_cout << d._year << "-" << d._month << "-" << d._day;
	return _cout;
}
int main(){
	Date d(2017, 12, 24);
	cout << d << endl;
	return 0;
}

注意:

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

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

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

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

  • 友元关系不能传递

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

class Date; // 前置声明
class Time{
	friend class Date; // 声明日期类为时间类的友元类,则在日期类中就直接访问Time类中的私有成员变量
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;
};

3.6 内部类

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

class A {
private:
	static int k;
	int h;
public:
	class B{
	public:
		void foo(const A& a){
			cout << k << endl;
			cout << a.h << endl;
		}
	};
};
int A::k = 1;
int main(){
	A::B b;
	b.foo(A());
	return 0;
}

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

特性:

  • 内部类可以定义在外部类的public、protected、private都是可以的。
  • 注意内部类可以直接访问外部类中的static、枚举成员,不需要外部类的对象/类名。
  • sizeof(外部类)=外部类,和内部类没有任何关系
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

xiaomage1213888

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值