我们知道,每个对象的数据成员都分别占有存储空间,如果对同一个类定义了N个对象,那么就会有N组同样大小的空间可以存放N个对象中的数据成员。那么问题来了当不同对象的成员函数引用数据成员时,编译器怎么能保证引用的就是所指定的对象的成员呢? 先看个例子:
#define _CRT_SECURE_NO_WARNINGS 1#include using namespace std;/*this指针调用步骤第一步: 识别类名 第二部: 识别类中的成员变量第三部: 识别成员函数,并对成员函数进行修改*/class Student{public: // this指针--->类对象 永远指向的是当前对象//void SetStudentInfo(Student* this, char* name, char* gender, int age)void SetStudentInfo(char* name, char* gender, int age){cout << this << endl; //谁调用了这个函数就指向谁strcpy(_name, name); //就相当于strcpy(this->_name,name);strcpy(_gender, gender);_age = age; //this放的是对象的地址 }void PrintInfo(){cout << _name << " " << _gender << " " << _age << endl;}private:char _name[20];char _gender[3];int _age;};int main(){Student s1, s2;s1.SetStudentInfo("鸣人", "男", 14);s2.SetStudentInfo("佐助", "男", 14);return 0;}
我们看看结果和在内存中是怎么操作的:
![8211ce35da80010c78c11e548de3ccfe.png](https://i-blog.csdnimg.cn/blog_migrate/e06ba516d78de6c3fdaed8278a21a5f3.jpeg)
![241024c7f59649ff9d1e872431bab9db.png](https://i-blog.csdnimg.cn/blog_migrate/3d253f8060abe3ee7a155f1f9ec52738.jpeg)
![c5d8c90d006909c1b2dffb2e3c6fb5fc.png](https://i-blog.csdnimg.cn/blog_migrate/a0b2b97941bfa172f4bb31aad4e90af8.jpeg)
从上述代码中我们可以看到,函数如果想要操作对象,就必须将类对象的地址传进来,否则没有办法对实参对象进行修改。因此,我们知道类的成员函数实际上也有一个隐藏的指针,指向了调用该函数的对象本身,我们把这个隐藏的指针就叫做是This指针,它的值是当前被调用的成员函数所在的对象的起始地址。 最后我们对这个指针特性做一个总结: ●this指针的类型:类类型 ***** const●this指针并不是类对象本身的一部分,因此不影响的sizeof的结果 ●this的作用域在类“成员函数”内部 ●this指针是“类的非静态成员函数”的第一个默认隐含参数,编译器默认为隐式使用 ●只有在类的非静态成员函数中才可以使用this指针,其他类型的函数都不可以。
这里引出两个 问题 :
⑴引用的底层也是指针,此处为什么不是引用而使用this指针?
答:这里就不得不说一下this指针的传参方式。 ①ecx寄存器:调用 _thiscall 这个调用约定,如果参数确定,this指针通过ecx传递给被调用者,但他只能用在参数确定的非静态成员函数上 ②参数压栈:参数从右向左压栈,如果参数不确定,则调用_cdecl这个函数调用约定,this指针在所有的参数被压栈后压入堆栈,完事需要调用者自行清理堆栈。
⑵this指针是否可以为空? 答:有可能 先看一段代码:
class Test{public:void FunTest(){cout << "FunTest():" << this << endl;}private:int _data;};int main(){Test t;t.FunTest();Test* pt = &t; //mov ecx ptpt->FunTest(); //call Test::FunTest()pt = NULL;pt->FunTest(); //Test::FunTest(pt(NULL))return 0;}
![70e77680a211bab25879373811e28c9a.png](https://i-blog.csdnimg.cn/blog_migrate/43a2cc58efd1ccefa678278cfcf7ce44.jpeg)
从结果当中我们可以看到this指针完全是可以有可能为空的,一旦在当前函数体中它访问了当前对象的成员变量或者数据,程序就会崩溃。
class Test{public:void FunTest(){cout << "FunTest():" << this << endl;_day=20;}private:int _day;};
![b19eefabdf4945b5470afa58c77d0c6f.png](https://i-blog.csdnimg.cn/blog_migrate/f44d3220a9037ef49d85f0309b5df34d.jpeg)
##类的六个默认成员函数## 在C ++ 98里类的默认成员函数有六个:构造函数,拷贝构造函数,析构函数,赋值操作符重载,取地址操作符重载,和const修饰的取地址操作符重载
但是在C ++ 11里面又添加了移动构造和移动赋值两个默认成员函数 这里我们只谈一谈C ++ 98里面涉及到的成员函数
构造函数
1。 概念:构造函数是C ++里为了解决对象初始化而产生的一个函数,比较特殊,名字和类名相同,创建类类型对象时由编译器自动调用,在对象生命周期内只调用一次,保证每个数据成员都有一个合适的初值。 下面我们举个例子说明一下:
class Date{public:Date(int year,int month,int day){_year =year;_month =month;_day= day;}void PrintDate(){cout << _year <
![64de92ab84378bc3523962334721104c.png](https://i-blog.csdnimg.cn/blog_migrate/316e7eda49cb4999e29ddd697af99d0a.jpeg)
2 构造函数实现对象的初始化
2.1在类内声明并初始化成员变量
class Time{public:Time(){_hour = 0;_minute = 0;_second = 0;}void SetTime();void PrintTime();private:int _hour;int _minute;int _second;};void Time :: SetTime(){cin >> _hour;cin >> _minute;cin >> _second;}void Time :: PrintTime(){cout << _hour <
在上述程序中,我们构造了时间类型的构造函数,在建立对象时自动执行构造函数,根据构造函数里面的定义,将该对象的全部成员值赋为0。
2.2 除了在类内定义构造函数,也可以在类内对构造函数进行声明而在类外定义构造函数:
Time(); //对构造函数进行声明//在类外定义构造函数Time::Time(){_hour = 0;_minute = 0;_second = 0;}
注:构造函数的使用 (1)调用构造函数的时机:在建立对象时编译器会自动调用构造函数,把指定的初值送到有关数据成员的存储单元中。每建立一个对象都会调用一次构造函数 (2)构造函数没有返回值也没有类型,它的作用只是对对象进行初始化 (3)构造函数不需要用户声明也不能被用户调用,如:
t.Time(); //试图用一般的成员函数的调用方法来调用构造函数,错误
(4)可以用一个类对象初始化另一个类对象,如:
Time t1; //建立对象T1,同时调用构造函数t1.Time()Time t2 = t1; //建立对象t2,并用t1初始化t2,这里只是把t1里面的数据成员复制到t2中,而不调用构造函数t2.Time();
(5)在构造函数中的函数体不仅可以对数据成员赋值,而且还可以包含其他语句,例如cout和cin语句。但是为保持程序的清晰度,一般不建议这样定义 (6)如果用户没有定义构造函数,那么系统会自动生成一个构造函数,只是这个构造函数的函数体是空的,没有参数,也不执行初始化操作 2.3带参数的构造函数 上述代码中构造函数不带参数,在函数体内对数据成员进行初始化,但有时候用户希望对不同的对象赋予不同的初值,上述代码就无法完成用户赋予的使命。那么我们应该怎么做呢? 那就是要讲的带参数的构造函数,在调用不同对象的构造函数时,从外面将不同的数据传递给构造函数,以实现用户想要的的初始化。 一般形式为:构造函数名(类型1形参1,形参2 ,,,,) 类名对象名(实参1,实参2 ,,,,,,) 我们举得第一个例子就是带参数的构造函数:
class Date{public:Date(int year,int month,int day){_year =year;_month =month;_day= day;}}
2.3.1用参数初始化列表对数据成员初始化(最常用)
这里应当注意的是: ●每个成员在初始化列表中只能出现一次(因为初始化需要划分空间,就跟你出生只能出生一次一样,但是可以多次赋值(跟起名字一样)) ●初始化列表仅适用于初始化类的数据成员,并不指定这些数据成员的初始化顺序,数据成员在类中的定义顺序就是在参数列表中的初始化顺序 ●要尽量避免用成员初始化成员,成员的初始化顺序最好和成员的定义顺序保持一致
如果类中包含以下几种成员,必须要放在初始化列表位置进行初始化: ●引用成员变量 ●const的成员变量 ●类类型成员(有构造函数,但不是缺省的构造函数) 给出例子:
class Date{public:Date(int year,int month,int day,int a,int b): _year(year),_month(month), _day(day), a(a), _b(b){}void PrintDate(){cout << _year << "." << _month << "." << _day<< endl;}private:int _year;int _month;int _day;int& a;const int _b;};
如果以上三种情况没有在初始化列表初始化,编译器会给我们报错,读者自行验证,这里不再验证 ** 3 ** 构造函数特性 ●函数名与类名相同 ●没有返回值 ●新对象被创建时,由编译器自动调用,并且是在对象的生命周期内仅调用一次 ●构造函数可以重载,实参决定了调用那个构造函数 ●无参构造函数和带有缺省值的构造函数都认为是缺省的构造函数,并且缺省的构造函数只能有一个 ●有初始化列表(可以不使用) ●若果没有显示定义,编译器会自动合成一个默认的构造函数(默认构造的函数一定不带参数)-------实际上没有默认的构造函数,只有在特定的场景下才会构造默认的构造函数(编译器感觉自己需要它就会自动合成) 特定的场景:
![3f541fca6050a126c5a8a87f01202558.png](https://i-blog.csdnimg.cn/blog_migrate/398494dcea2bf23e84523290b150fd0d.jpeg)
class Date{public:/*Date(){}*/private:int _year;int _month;int _date;//Time _t;};class Time{public:Time(int hour=0,int minute=0,int seconds=0): _hour(hour), _minute(minute), _seconds(seconds){}private:int _hour;int _minute;int _seconds;};int main(){Date d; //为什么不能在这里直接调用Time类的构造函数?//因为我们创建的是Date类的对象,怎么可能会调用Time类的构造函数return 0;}
![cc81a779a8de634eec4a4d450087bb07.png](https://i-blog.csdnimg.cn/blog_migrate/e7a1ca98db67b341498545524c41f0b9.jpeg)
在这里我是给319行下了断点但是程序并没有停下来,如果将Date类里的 Test _t 放开的话,程序便能停下来。
●构造函数不能用const的修饰(为什么?) 答:构造函数需要修改成员变量的值,如果用const修饰,与功能不符,互相矛盾 ●构造函数不能为虚函数
** 4构造函数的作用** ●构建对象 ●初始化对象 ●类型转换 对于单个参数的构造函数,可以将其接受的参数转化为类类型的对象。用明确的修饰构造函数,抑制由构造函数定义的隐式转换,明确的关键字内部的构建声明上,在类的定义体外部的定义上不再重复。
##拷贝构造函数## 1 概念 只有单个形参,而且该形参是对本类类型对象的引用(常用const修饰),这样的构造函数称为拷贝构造函数。拷贝构造函数是特殊的构造函数,创建对象时使用已存在的同类对象来进行初始化,由编译器自行调用 看个例子:
class Date{public:Date(int year,int month,int date)//构造函数:_year(year),_month(month),_date(date){}Date(const Date& time)//拷贝构造函数:_year(d._year),_month(d._month),_date(d._date){}private:int _year;int _month;int _date;};int main(){Date d1(2018,7,23);Date d2(d1);return 0;}
![984d495c10b5ed472a3cbcc17afdf225.png](https://i-blog.csdnimg.cn/blog_migrate/d3a5c5ffd2839431a2b3041be21d7269.jpeg)
2拷贝构造函数特征 ●拷贝构造函数继承了构造函数的所有性质 ●参数必须使用类类型对象引用传递,(是因为如果传值的话,会创建一块临时空间,直到空间爆满(栈溢出),但值并没有传进去,只是开辟了一块临时空间) ●如果没有显式定义,系统会自动生成一个默认的拷贝构造函数。默认的拷贝构造函数会依次拷贝类的数据成员完成初始化。 ** 3适用场景** ●作为函数参数 ●对象实例化对象 ●作为函数返回值
##析构函数##
1概念:同析构函数一样,析构函数也是一个特殊的成员函数,作用与析构函数相反,名字是类名前面加一个“~”,在对象的生命周期结束时,由编译器自动调用,完成类的一些资源清理工作。 举个栗子:
class Student{public:Student(char* name, char* gender, int age):_name(name), _gender(gender), _age(age){cout << "Student Called" << endl;}void PrintInfo(){cout << "_name:"<<_name endl _gender _age called _name main s1 s2>
![fa9354e827232ab5775a1ddff012f1b0.png](https://i-blog.csdnimg.cn/blog_migrate/fc2fe01eb7923d60fe454963a8fcddbc.jpeg)
解读一下这段代码:在main函数前面申明类,作用域是全局的。在学生类中定义了构造函数和析构函数。在执行main函数时先建立对象s1,在建立对时调用对象s1的构造函数并给该对象中的数据成员赋初值,然后调用PrintInfo函数,输出s1的数据成员的值。接着建立对象s2,在建立对象时调用s2的构造函数,然后调用s2的PrintInfo函数,输出s2的数据成员值。由于在主函数建立的对象是局部的,生命周期随着主函数结束而结束,在撤销对象之前调用析构函数。 在结果中我们看到了两个一模一样的析构函数,那么到底是怎么区分他们分别是谁的析构函数呢?
2 调用析构构和和构造函数的顺序 最先被调用的构造函数,其对应的析构函数最后被调用,后被调用的构造函数,其对应的析构函数最先被调用,可以简记为先构造的后析构,后构造的先析构,相当于一个栈,先进后出。 看到这,我们就应该明白,结果中的第一个析构函数是s2的,第二个析构函数是s1的。
3 调用析构函数的场景 ●如果在一个函数中定义了一个对象(假设是自动局部对象),当这个函数被调用结束是系统会在对象释放前自动调用析构函数 ●静态局部对象在函数调用结束后对象并不会被释放,因此也不会调用析构函数。只有在主函数结束或者退出强制结束程序时才会调用静态局部对象的析构函数 ●如果定义的是全局的对象,则在程序离开其作用域时才会调用该全局对象的析构函数
##运算符重载## 先提一个问题:在C++中能否用“+”直接来进行两个复数的相加?
1 概念:重载操作符是具有特殊函数名的函数,关键字operator后面接需要定义的操作符符号。操作符重载也是一个函数,具有返回值和形参表。
下面是C++允许重载的运算符
![59714f56c2bc6088caa0416b6e8b8a99.png](https://i-blog.csdnimg.cn/blog_migrate/047cea8e2c44fed265d6f036b7a73739.jpeg)
** 不能重载的运算符只有五个: **
![1e05aa0fa58444a774266d27ebcce79b.png](https://i-blog.csdnimg.cn/blog_migrate/ad9f9414019bffc672ed48a45abc5b62.jpeg)
重载运算符规则: ⑴C++不允许用户自己定义新的运算符,只能对已有的C++运算符进行重载 ⑵重载不能改变运算符运算对象(即操作数)的个数,如关系运算符“>”是双目运算符,重载后仍然是双目的 ⑶重载不能改变运算符的优先级别 ⑷重载不能改变运算符的结合性 ⑸重载运算符不能有默认的参数 ⑹重载的运算符必须和用户定义的自定义类的对象一起使用,参数至少应有一个是类对象(或者对象的引用) ⑺用于类对象的运算符一般必须重载,但有两个例外,运算符“=”和“&”不必用户重载 ⑻从理论上讲,可以将一个运算符重载为执行任意的操作,不过不建议这样使用,会混淆视听,使人难以理解程序 规则很容易理解,不必死记。
运算符重载需要注意的点: ●不能通过连接其他符号来创建新的操作符,如operator@ ●重载操作符必须有一个类类型或者枚举类型的操作数 ●用于内置类型的操作数,其含义不能改变,例如:内置的类型+,不能改变其含义 ●作为类成员的重载函数,其形参看起来比操作数数目少一个成员函数的操作符有一个默认的形参this,限定为第一个形参 ●一般将算术操作符定义为非成员函数,将赋值运算符定义为成员函数 ●操作符定义为非类的成员函数时,一般将其定义为类的友元 ●== 和 !=操作符一般要成对重载 ●下标操作符[]:一个非const成员并返回引用,一个是const成员并返回引用 ●解引用操作符 * 和 ->操作符,不显示任何参数 ●前置式++/--必须返回被增量或者减量的引用,后缀式操作符必须返回旧值,并且应该是值返回而不是引用返回 ●输入操作符 >>和输出操作符 << 必须定义为类的友元函数