引言
this指针
问:函数都保存在公共代码区,调用时是如何分辨出是哪个对象调用?
这就要引出this指针了,其实是有个隐含的this指针。
a.每个成员函数都有一个指针形参,它的名字是固定的,称为this指针。(this指针是隐式的,构造函数没有this指针)
b.在对象调用成员函数时,编译器会将对象的地址作为实参传递给成员函数的第一个形参this指针。
c.this指针是成员函数隐含的指针形参,我们不能越俎代庖,随意的添加this指针,
eg:我们定义一个日期类
#include <iostream>
using namespace std;
class Date
{
public:
void Display()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
下图解释:
问题
那么还有一个问题this指针是存放在哪里的?堆、栈、还是全局变量?还是其他?
this指针会因为编译器的不同而有不同的放置位置,可能是栈,可能是寄存器,甚至可能是全局变量,在汇编级别里面,一个值会有三种形式存在:立即数、寄存器值,和内存变量值,不是存放在寄存器就是存放在内存中,它们并不是和高级语言进行对应的。
this指针是如何进行参数传递的?绑定?还是在函数参数的首参数就是this指针?那么“this指针是如何找到类实例后函数的”?
大多数的编译器通过ecx寄存器传递this指针。事实上,这也是一个潜规则,一般来说不同的编译器都会遵从一致的传参规则,否则不同编译器产生的obj就无法匹配了。
在call之前,编译器会把对应的对象地址放到eax中,this是通过函数参数的首参来传递的。this指针在调用之前生成,至于“类实例化后函数”,没有这个说法,类在实例化时,只分配类中的变量空间,并没有为函数分配空间。自从类的函数定义完成后,它就在那,不会跑。
默认成员函数
一、构造函数
成员变量是私有的,要对它们进行初始化,必须用一个公有成员函数来进行。同时这个函数有且仅在定义对象时自动执行一次,这时调用的函数成为构造函数(constructor)。
特点
- 函数名与类名是相同的
- 无返回值
- 对象构造(对象实例化)时系统自动调用对应的构造函数
- 构造函数可以重载
- 构造函数可以在类中定义,也可以在类外定义
- 如果定义中没有给出构造函数,则C++编译器自动产生一个缺省的构造函数,但我们只要定义了一个构造函数,系统就不会自动的生成构造函数。
- 无参的构造函数和全缺省的构造函数都认为是缺省构造函数,并且缺省构造函数只有一个
#include <iostream>
using namespace std;
class Date
{
public:
Date()//无参构造函数
{
_year = 2018;
_month = 7;
_day = 8;
}
Date(int year = 1900, int month = 1, int day = 1)//全缺省构造函数
{
_year = 2018;
_month = 7;
_day = 8;
}
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(2018, 7, 8);
date.print();
system("pause");
return 0;
}
二、拷贝构造函数
创建对象的时使用的同类对象进行初始化,这时所使用的构造函数成为拷贝构造函数(copy C onstructor)拷贝构造函数是特殊的构造函数。
特点
- 1、拷贝构造函数其实是一个构造函数的重载。
- 2、拷贝构造函数的参数必须使用引用传参,使用传值方式会引起无穷递归调用。
如果拷贝构造函数中不采用传引用的方式进行传参的话,在使用传值的方式调用该类的拷贝构造函数,从而造成调用该拷贝构造函数,从而造成无穷递归的调用该拷贝构造函数,因此拷贝构造函数的参数必须是一个引用。
需要澄清的是传指针的方式也是进行传值的方式,在这里我们需要了解的是一般下只有传引用不是传值的方式外,其他的传参的方式都是进行传值的方式。
- 3、若未显示定义,系统会默认缺省的拷贝构造函数,缺省的拷贝构造函数会依次拷贝类成员进行初始化。
eg:
#include <iostream>
using namespace std;
class Date
{
public:
Date()
{
}
//拷贝构造函数
Date(const Date& d)
{
_year = 2018;
_month = 7;
_day = 8;
cout << _year<<"-"<< _month << "-"<<_day << endl;
}
private:
int _year;
int _month;
int _day;
};
void testDate()
{
Date d1;
Date d2(d1);//调用拷贝构造函数
Date d3 = d1;//调用拷贝构造函数
}
int main()
{
testDate();
system("pause");
return 0;
}
结果如下:
另外我们需要注意的是函数的返回值不是引用也有很大的区别,只是一个简单的对象的时候,此时就需要调用拷贝构造函数,否则如果是引用的话就不需要调用拷贝构造函数。
#include <iostream>
using namespace std;
class A{
public:
A()
{
}
A(const A& a)//拷贝构造函数
{
_test = a._test;
cout << "copy constructor" << endl;
}
A& operator=(const A& a)
{
if (this != &a)
{
_test = a._test;
cout << "copy Assign" << endl;
}
return *this;
}
private:
int _test;
};
A fun(A& x)
{
return x;
}
int main()
{
A test;
fun(test);
system("pause");
return 0;
}
运行之后的结果就可以证明这一点
小练习:
class A
{
private:
int value;
public:
A(int n)
{
value = n;
}
A(A other)
{
value = other.value;
}
void Print()
{
cout<<value<<endl;
}
};
int main(void)
{
A a = 10;
A b = a;
b.Print();
return 0;
答案:编译错误,在拷贝构造函数中传入的参数是A的一个实例。由于是传值,把形参拷贝到实参会调用拷贝构造函数。因此如果允许拷贝构造函数传值,那么就会形成永无休止的递归并造成栈溢出。因此C++的标准不允许拷贝构造函数传值参数,而必须是传引用或者常量引用,在Visual Studio和Gcc中,都将编译出错。
三、析构函数
当一个对象的生命周期结束时,C++编译系统会自动调用一个成员函数,这个特殊的成员函数就是析构函数(destructor)。
特点:
- 1、析构函数在类名前面加上字符 ~
- 2、析构函数无参数无返回值
- 3、一个类有且只有一个析构函数,若未显示定义,系统会自动生成缺省析构函数。
- 4、对象声明周期结束时,C++系统会自动的调用析构函数
- 5、析构函数体内并不是删除对象,而是做一些清理工作。
注意:我们写的类没有那么复杂的时候就不需要写析构函数。如果一个类只有有这些事情:打开文件、动态分配内存、连接数据库,简单的说就是只要构造函数中有new
这个关键字的时候我们就需要写析构函数。
eg1:
class Array
{
public:
Array(int size)
{
_ptr = (int*)malloc(size*sizeof(int));
}
//这里的析构函数主要做的事情就是释放空间
~Array()
{
if (_ptr)
{
free(_ptr);
_ptr = 0;
}
}
private:
int* _ptr;
};
eg2:
此题必须注意:
1、执行构造函数与析构函数的两种定义方式(一种要动态分配内存,然后清除资源,再执行析构函数,另一种直接定义构造函数,执行完构造函数后自动执行析构函数);
2、析构函数执行的过程是从后往前执行!
class A {
public:
A() {
printf("constructing an object of A\n");
}
~A() {
printf("Destructing an object of A\n");
}
};
int main() {
printf("----begin main---\n");
A b;//使用这种方式,不需要清除资源,会自动执行析构函数
A *c = new A;
delete c;
printf("*******\n");
A *d = new A;//动态分配内存,后需要用delete清除资源,然后执行析构函数
A *e = new A;
printf("----end main---\n");
delete e;//清除资源,然后执行析构函数
delete d;
//(不用动态分配内存,不用清除资源,就能在构造函数执行完后自动执行析构函数)
A d1;
A e1;
printf("----end main---\n");
system("pause");
return 0;
}
运行结果如下:
运算符重载的特征:
- 1、operator+合法的运算符 构成函数名(重载<运算符的函数名:operator<);
- 2、重载运算符以后不能改变运算符的优先级/结合性/操作数个数
5个C++中不能重载的运算符
.*/::/sizeof/?:/.
四、赋值运算符重载
拷贝构造函数是创建的对象,使用一个已有对象来初始化这个准备创建的对象。
赋值运算符的重载是对一个已存在的对象进行拷贝赋值。
我们在进行赋值运算符的时候必须注意的几个问题:
- 1)返回值的类型声明为该类型的引用,并在函数结束前返回实例自身的引用(*this),这样是为了完成连续的赋值,如果函数的返回值是void,就不能进行连续的赋值。
- 2)传入的参数类型声明为常量引用。(这样是为了减少一次拷贝构造函数,能提高代码的执行的速度)还有一点我们在赋值操作运算符内不会改变实例的状态,因此我们在传入的参数前面加上const关键字。
- 3)释放实例中自身的内存,如果我们忘记在分配之前释放自身已有的空间,这样将会出现内存泄漏。
- 4)传入的参数必须和当前的实例(*this)不是同一个对象,如果我们不进行判断那么我们当*this和传入的参数是同一个实例时,一旦我们释放了自身的内存,传入的参数的内存同时也被释放了,因此再也找不到要进行赋值的对象。
eg1:
mystring& mystring::operator=(const mystring &str)//赋值操作运算符重载
{
if (this == &str)//判断传入的参数和当前的实例(*this)是同一个对象
{
return *this;
}
delete[]m_pData;
m_pData = NULL;
m_pData = new char[strlen(m_pData) + 1];
strcpy(m_pData, str.m_pData);
return *this;
}
在这里我们需要注意的是
delete释放的是new分配的单个对象的指针指向的内存;
delete [ ]释放的是new分配的对象数组指针指向的内存。
我们在写完上面这个一般的初级的赋值运算符的重载之后,我们的代码可能还会出现安全的问题,这就进一步的需要我们进行重新的进行思考的代码的严谨性。就如eg2所示:
eg2:
mystring& mystring::operator=(const mystring &str)
{
if (this != &str)
{
mystring strTmp(str);
char* pTmp = strTmp.m_pData;
strTmp.m_pData = m_pData;
m_pData = pTmp;
}
return *this;
}
在上面的这段代码中我主要考虑的就是定义一个新的临时实例strTmp,然后将它的strTmp.m_pData和实例自身的m_pData进行交换,然后利用strTmp是一个临时变量,在出了作用域之后,就会调用strTmp自身的析构函数,把strTmp.m_pData所指向的内存进行释放掉。由于我们的strTmp.m_pData所指向的内存是指向之前的m_pData内存,所以就间接的相当于自动的调用析构函数释放实例中的内存。
五、初始化列表
类的成员变量有两种初始化的方式
- 1、初始化列表
- 2、构造函数体内进行的赋值
那么哪些变量必须放在初始化类表当中
1)常量成员变量(常量创建的时候必须进行初始化);
2)引用类型成员变量()
初始化列表是一个以冒号开始的,接着一个逗号进行分隔数据列表,每个数据成员都放在一个括号中进行初始化,我们一般尽量使用初始化列表进行初始化因为它更加的高效。
- 成员变量按声明顺序依次进行初始化,而非初始化列表出现的顺序。
class Date
{
public:
Date(int x)
:_day(x)
, _month(_day)
, _year(x)
{
cout << "Date()" << endl;
}
void Display()
{
cout << "year-》" << _year << endl;
cout << "month-》" << _month << endl;
cout << "day-》" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(1);
d1.Display();
system("pause");
return 0;
}
结果是:
在这里更加证实了成员变量按声明的顺序依次初始化,,而非初始化列表出现的顺序。
我们换应该需要的认识的是声明与定义的区别(可能是我们要遇到过的重要的知识点)
对于变量来说
1)定义变量:指明变量所属类型,名称,分配内存空间与初始化其初始值。
int a=1;
int a(1);
如果不显示初始化,则按照编译器默认进行初始化。
2)声明变量:指明变量所属的类型与变量名称。
extern int a;
有一点我们需要注意的是,对于局部变量来以及全局静态变量是不能通过extern 进行前置声明的,既不能在定义之前通过声明来引用,因为局部变量的作用域是当前代码块,全局静态变量的作用域是当前的源文件,都不是全局作用域,所以不能通过extern进行前置声明,全局变量允许在定义之前通过前置声明进行引用,参见下面的代码:
#include <stdio.h>
extern int a;//报错
extern static int b;//报错
int main()
{
extern int c;//报错
printf("a=%d,b=%d,c=%d",a,b,c);
int c=2;
}
int a=2;
static int b=1;
对于类型来说:
1)定义类型:指明类型和名称的内容
struct test
{
int a;
}
或者是给已经存在类型进行起名字
typedef int int32;
2)声明类型:只给出类型的名称
class A;