C++————this指针&类的6个默认成员函数

本文详细解析了C++中this指针的概念及其存放位置,成员函数调用机制,构造函数、拷贝构造函数及析构函数的特点与应用,并介绍了赋值运算符重载的方法与初始化列表的使用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

引言

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=1int 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;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值