C++基础——类和对象:封装和对象特性

本人的C++学习笔记,内容杂乱冗长,也并不专业

封装

将属性和行为绑定为一个整体,加以权限控制,并通过对象完成操作;对外提供接口,屏蔽数据,对内开放数据。

成员权限

修饰符权限说明
public 公共权限成员类内可以访问、类外可以访问
protected 保护权限成员类内可以访问、类外不可以访问(儿子可以访问父亲中的保护内容)
private 私有权限成员类内可以访问、类外不可以访问(儿子不可以访问父亲中的保护内容)

同一对象的私有成员变量可以通过公共成员方法来访问,提供读/写等接口,进一步控制权限。

尽量把成员变量都写成私有权限,降低成员权限,提高安全性。这样在我们访问时就只剩下了公共成员里的函数,尤其在变量和函数众多、命名类似时,只能调用函数可以避免思考到底是函数还是变量,要不要加上(),而且可以更精细地控制权限,以及对读写添加额外条件,如禁止输入小于0和超过150的年龄。

结构体的成员默认是公有的,而类的成员默认是私有的。

对象的构造和析构

构造函数

不同创建方式会调用不同的构造函数,也可作为对应指针的接收表达式。

**默认构造:**类名 对象名();小括号可省Student stu;

**有参构造:**类名 对象名(参数列表);或类名 对象名={参数列表};

Student stu(“Ame”,20); Student stu={“Ame”,20};

Student* stu1 = new Student( “ame”, 20 );

{}的=号可以省略,{}是C++11引入初始化列表后的方法,具有更严格的类型检查,而传统的()会做隐式类型转换,尽量用{}更安全。

**拷贝构造:**类名 对象名(拷贝对象);或类名 对象名=拷贝对象;

Student stu2(stu1); Student stu2=stu1;

new给对象开辟空间

在创建一个C++对象时要做两件事:1、给对象分配内存;2、调用构造函数来初始化那块内存。如果你不写构造函数,默认的是空的,这就是一种未初始化的对象,极易导致程序出错。

其实宽泛地来说这是创建一个变量时都会有的操作:分配内存,初始化,如果只是普通的变量将会很好处理。

而在创建类的对象时,我们经常要用指针来接收其首地址,即需要用动态分配内存的指令,在C中提供了malloc()和free()及其变体函数,这对结构体和类都是有效的,故第一件事是容易保证的,但初始化还是要自己再手动做。故C++提供了new和delete运算符,相比C不但带有内置的长度计算、类型转换和安全检查,更重要的是把这两件事结合在一起,new每次开辟自定义类型的内存空间时,除了分配内存还必定会调用其构造函数,delete则会在释放内存之前调用析构函数。注意,构造函数是在创建类的对象时才会自动调用,只创建一个类的对象指针是不会调用其构造函数的。

如果涉及到要使用类的指针,如Student* student2;只创建了一个该类的指针变量student2,并不会调用其构造函数,哪怕在默认构造函数里有开辟空间和初始化操作也不会有效果,即使换成有参构造Student* student2(“Ame”, 20);或者拷贝构造Student* student2(student1);也不行,指针必须手动开辟空间和初始化,或者先创建一个对象,调用了构造函数再用指针去接收,亦或者直接用new,这也是最推荐的。

在C中用malloc()可以省略等式右值的类型强转,但C++中有严格的类型检测,如果要用的话不可省略。

拷贝构造函数的调用时机

由于值传递本身是一种拷贝临时变量的操作,所以当以值传递的方式给函数传递类形参时会拷贝实参对象,从而调用拷贝调用。

同样的,以值方式返回局部对象时,因为局部对象调用后要销毁,故返回的也是拷贝来的新变量,也会有拷贝调用。

故即使不写,默认除了空构造和析构函数,C++编译器还会默认添加值拷贝构造函数。

浅拷贝和深拷贝

主要针对对象或结构体变量指针而言。浅拷贝就是简单的值传递,如果存在指针(比如对象一般都是指针),则也只会复制指针,新旧将共用一个地址。那么比如在释放的时候,可能会重复释放导致第一个内存空间已经被释放了,此后第二个指针变成野指针。

深拷贝则是重新开辟一个空间,再将里面的值复制。

编译器默认的拷贝构造函数,还有赋值号=就是浅拷贝,如下:

Person p1(18, 160);

Person p2(p1);

在这里插入图片描述

如下图所示,p2是浅拷贝的p1,普通变量m_Age没影响,但指针变量m_Height在该段程序结束时,按栈依次调用p2的析构,已经将0x0011的空间释放掉,但只将p2.m_Height置空,p1.m_Height还是指向0x001,成为野指针,再次调用p1的析构时通过判断继续释放,故报错,且如果p2有其他初值,用过默认的赋值符=来p2=p1,还会导致原来的p2.m_Height指向的空间无法访问,内存泄漏。

所以这种类在堆区开辟成员的情况要自己提供深拷贝的构造函数(和赋值运算符=的重载函数,对应节有提到),如下:

在这里插入图片描述

如果是一个数组之类的,还要依次把原数组的数据也拷贝过去,如:

在这里插入图片描述

这样再配合析构函数,各自释放自己的指针,运行成功。

初始化列表

构造函数的一种语法糖,括号后冒号+成员属性(变量/常量)+。。。

Person(int a,int b,int c) :m_A(a), m_B(b), m_C(c){
	其他构造函数内容;
}

当成员中有引用或常量时,需要用初始化列表来赋初值,如其中a/b/c可直接改成常量10。据说效率更高。

出现类的嵌套

当本类成员含有其他类对象时,会先构造其他类对象,最后构造本类。

析构函数则反过来先执行本类的析构,再执行所包含的其他类对象的析构,整体上是一个栈的思想,先进后出:调用时本类在前,他类在后,故构造时本类最后,销毁时他类在前,本类在后,故析构时本类最先。

这种顺序有利于安全性,即创建时先把内层都做好,再构造外层,销毁时先销毁外层,再销毁内层,如若反之,可能会出现外层有了,但还无法访问内层。

同一个函数下创建多个类,在该函数调用结束并销毁这些类时,它们的析构函数也是按栈的形式后入先出的调用。

静态成员

静态成员变量(属性)

静态成员变量不属于某个特定对象,同一类下的不同对象共享一份静态成员数据,一改全改,编译阶段就已在全局区分配内存。

由于是成员变量,static int m_A肯定是在类内声明,同时一定要在类外做初始化,即在全局int Preson::m_A=10,可以不赋值,默认为0。

即可通过对象p.m_A,也可通过类名Person::m_A访问,不用创建对象,但前提是该静态成员是公共权限。

静态成员函数(方法)

基本和静态成员变量相同,但因为是函数,不需要类外初始化。

静态成员函数只能访问静态成员变量,很好理解,因为静态成员函数也可以在无对象的时候类外直接访问(前提是公共权限),若访问其他非静态成员,无法确定是哪个特点对象的成员,且静态在编译阶段就已经分配内存,此时也无法找到非静态成员。

成员变量和成员函数分开存储

C++中所属类内成员变量和成员函数是分开储存的,类内只存储非静态成员变量,成员函数地址早绑定,和静态成员一样可以类内声明,类外定义(初始化)。而只有非静态成员变量才属于特定的类的对象,函数和静态成员变量都只属于类本身。

成员函数通过对象指针调用成员变量,sizeof(对象)只算成员变量大小。

编译器会给每个空对象也分配一个字节的独一无二的地址来占位,防止不同的空对象被实例化后占到同一个内存地址上。当非空时,这个1字节地址会被覆盖掉。

通常类内函数都会在类外实现,类内实现的函数,如果足够短小、简单,且没有对函数取址操作,编译器会自动对其内联编译,宏展开。

this指针

this指针的作用

前文可知即使是非静态成员函数,也不属于特定的对象,只会诞生一份函数实例,多个同类型的不同对象会共用一份代码,C++通过this指针来区分当前对象。this指针会自动指向当前被调用的成员函数所属对象,可用*this解引用返回该对象。

当p1调用了函数,this会指向p1,*this则就是p1。

this指针隐含在该成员函数内,不用定义,可直接使用。

this指针是一个指针常量,本质等价于Person * const this;指向的地址无法更改,指向地址的值可以更改。即可以this->age=10;但不可人为使this=xxxx。

若再用const修饰成员函数,本质是修饰this指针,即等价于const Person*const this,此时连this->age=10也变成非法。

主要用途:

1.当形参和成员变量同名时用以区分,this->age=age,这里等价于Person::age=age,变量名称不冲突时可以不加,如m_age=age,但其实也默认加了this->,类似python的self.age,建议不太好区分的都加上,尤其类外实现,便于明确变量关系,敲代码有->提示还更快。

2.需要返回对象本身时用return *this,如链式编程,输入与输出同类

Person& PersonAddAge(Person &p){
	this->age += p.age;
	return *this
	//p2.PersonAddAge(p1)
	//p就是p1,this就是指向p2的指针
	//返回的*this就是p2这个对象
}
//输入与输出一致,链式编程
p2.PersonAddAge(p1).PersonAddge(p1).PersonAddAge(p1);

Q:与深浅拷贝串联起来,为何这段代码要用&引用传递?

因为如果不是引用传递,会默认浅拷贝,即值传递,此时返回的类型不是别名,return *this的是复制的一个匿名的新p,之后是对这个匿名对象再进行后续操作,但已经和实参p2的值无关了。

而加上&使用引用传递,每次返回的也一直是p2的别名,后续操作的地址还是p2本身,故输出p2时可以保留下来,且可以进行链式编程,尤其在后面的运算符重载常用。

前面定义返回值的&有无,会决定返回的是实参的别名还是拷贝值。

后面括号里的形参&有无,会决定传入的是实参的别名还是拷贝值。

空指针访问成员函数

空指针也是可以访问成员函数的

Person *p =NULL;

p->func();

但前提是func()中不能含有this指针,因为当使用到this指针时就需要this有一个给具体对象分配的非空地址。

注意成员变量其实都默认加了this->。可在该函数里加一个判断this是否为空的分支,使程序更加健壮。

友元

在类的开头添加以下代码,可以使其访问该类的私有内容:

如全局函数goodGay() /类goodGay/goodGay成员函数visit()想要访问Building类里的私有成员,故在类Building开头添加如下声明:

全局函数做友元:friend void goodGay(Building *building);

类做友元:friend class goodGay;

成员函数做友元:friend void goodGay::visit();

即在需要访问的类开头加上friend前缀关键字的相关声明即可。

注意,这里都是写在权限外面,即类的最开头,可以发现全局函数和成员函数做友元形式上的区别就在于有没有写类的作用域。这里的例子都只是类内声明、类外实现,实际上可以直接在该类里写实现,如friend void goodGay(Building *building){xxx};这种形式其实是全局函数的类内实现,从形式上可以理解为,类内没写在权限里的无显式作用域的函数,前面加上friend,则为全局函数,有作用域的,则为其作用域的成员函数,都不属于该类,是该类的友元。

运算符重载

运算符重载就是对你所定义的运算函数进行统一命名和简化调用,使已有的运算符可以适用新的数据类型。

注意,当重载函数需要访问私有成员时,除了通过公共成员方法来访问,还可以利用前文的友元,更方便。

C语言是没有重载的,所以想解决C语言里的类似问题要自己写个新函数。

加号+运算符重载

用全局函数重载+号:

Person operator+(Person &p1, Person &p2){
	Person temp;
	temp.m_A = p1.m_A + p2.m_A;
	temp.m_B = p1.m_B + p2.m_B;
	return temp;
}
//此后Person类的对象可直接做加法,p3=p1+p2,编译器会转换成p3=operator+(p1,p2)

用成员函数重载+号:

Person operator+(Person &p){
	Person temp;
	temp.m_A = this->m_A + p.m_A;
	temp.m_B = this->m_B + p.m_B;
	return temp;
}
//因为成员函数本身会获得自身对象的参数,故加法只需再传一个加数
//此后即可直接p3=p1+p2,编译器会转换成p3=p1.operator+(p2)

同时operator+()作为函数还可以函数重载,如再写一个:

Person opertor+(int num){
	Person temp;
	temp.m_A = this=->m_A + num;
	temp.m_B = this=->m_B + num;
	return temp;
}
//此后可以p4=p1+10,编译器则会调用这段重载。

理论上可以在+号重载成减法除法等,但会严重破坏代码可读性。

左移<<运算符重载

<<的重载常用于如cout<<p,配合友元实现自定义数据类型的输出。

为了符合cout在左,对象p在右的这一形式,不能使用成员函数来重载,因为最后编译器简化的格式是按从左到右,参数+符合+参数来排,成员函数中p作为对象本身一定在左边,也能实现效果,但是会是p<<out形式。所以本例只能用全局函数重载左移运算符:

ostream &operator<<(ostream &cout, Person &p){
	cout << "m_A=" << p.m_A << "m_B=" << p.m_B;
	return cout;
}
//此后Person类的对象可以有以下操作
cout << p << endl;

Q:为何要用ostream类型?

ostream类型是cout的数据类型,可以通过查找定义得知其是iostream的对象,因为cout本身也是一种可链式编程的对象,重载<<符号后为了使cout还能继续接<<,要保证重载函数的输出仍为cout的类型。

例如使用的是void,则在调用完cout<<p后不能再接<<,包括endl。

Q:为何要用&?

需要保证参数类型是ostream &引用传递,否则是浅拷贝,运行完cout<<p就会销毁,同时返回值类型也是ostream &,满足这种运算符的链式编程需求,当然这里的cout可以改成其他的,不影响调用cout<<p,因为本身是对<<的重载,cout只是一个典型案例。

递增++运算符重载

class myinteger{
	//重载<<运算符全局函数做友元
	friend ostream& operator<<(ostream &out, myinteger myint);
public:
	myinteger(){
		m_num = 0;
	}
	//重载前置++运算符
	myinteger& operator++(){//返回自己的别名
		m_num++;//递增改变自己的m_num值
		return *this;//再把自己返回
	}
	//重载后置++运算符,int占位符可用于编译器区分前置和后置
	myinteger operator++(int){//调用无需传入值
		myinteger temp = *this;//记录当前的自己
		m_num++;
		return temp;//返回临时变量的值,在++前
	}
private:
	int m_num;
};

注意,前置++定义的是引用别名,就像前文提到,是为了返回实参的别名,以保证可以链式编程,如cout << ++(++(++myint)) << endl;

而后置++由于是用临时变量保存的,调用结束后temp会销毁,只能浅拷贝,传递temp的值,故后置++无法链式编程,自带的++也一样。

赋值=运算符重载

编译器还会给类提供一个赋值运算符operator=,使得可以p1=p2进行类值的拷贝,但和默认拷贝构造函数一样,只是浅拷贝,当类中出现变量指向堆区时,就会出现深浅拷贝的问题,故此时我们要重载=。

例子上基本与“浅拷贝和深拷贝”一节类似,具体内容如下:

class Person{
public:
	Person(int age){//有参钩子献给m_Age开辟一个堆空间并赋初值age
		m_Age = new int(age);
	}
	int* m_Age;
	~Person(){//析构函数释放堆空间
		if(m_Age != NULL){
			delete m_Age;
			m_Age = NULL;
		}
	}
	//重载赋值运算符=
	Person& operator=(Person &p){
		//m_Age = p.m_Age;系统提供的浅拷贝
		if(m_Age != NULL){//如有初值先释放
			delete m_Age;
			m_Age = NULL;
		}
		m_Age = new int(*p.m_Age);//给=右值p的m_Age空间上的内容开辟新空间并赋值
		return *this;//深拷贝完成,返回自己以便=的链式编程
	}
};

//搭配<<重载
ostream& operator<<(ostream& cout, Person& p) {
	cout << "m_Age=" << *p.m_Age;
	return cout;
}

int main() {
	Person p1(18);
	Person p2(20);
	Person p3(10);
	p3 = p2 = p1;
	cout << p3 << p2 << p1;
}
//输出三者的成员属性m_Age均=18

调用时先是有参构造,然后再用=来拷贝赋值。在那一节中,我们通过重写构造函数,改成开辟堆空间的深拷贝来解决,这一节则是重载=运算符,除了一样改成深拷贝外,由于可能有初值,还需先释放掉原来的内存。

关系运算符重载

对<、>、==的重载,用于比较类之间的关系,很简单,如判断成员变量string m_name是否相等:

bool operator==(Person& p)

{

if (判断条件,本例为this->m_name == p.m_name)return true;

else return false;

}

还可以直接化简成return this->m_name == p.m_name;

数组下标[]重载

封装一个通用的数组类模板myArray时,为了赋予其能够和自带数组一样利用[下标],可以重载[],从而实现myArray[i],类中有this->pAddress=new T[容量];

T& operator[](int index){
	return this->pAddress[index];
}

函数调用运算符()重载—仿函数

即重载小括号(),重载后也非常像函数调用,故也称为仿函数,本质还是一个类的对象,故也叫函数对象。其本身的形式没有固定写法,非常灵活,在STL中经常使用。

仿写print()函数:

class MyPrint{
public:
	//重载函数调用运算符()
	void operator(){string test){
		cout << test << endl;
	}
}

MyPrint myPrint;//创建对象
myPrint("hello world");//直接使用
for_each(v.begin(), v.end(), myPrint());//本质是个类对象,可以作为函数参数,注意和函数指针不同的是要加()

调用时很像函数,但在编译器中小括号颜色与函数调用的小括号不同。

再如仿写加法函数:

class MyAdd{
public:
	int operator()(int num1, int num2){
		return num1 + num2;
	}
};

MyAdd myAdd;
int ret = myAdd(100, 100);
//或者用匿名函数对象
cout << MyAdd()(100, 100) << endl;

通过匿名函数对象,不保留实例,调用完立即销毁,但可以输出结果。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值