C++ this 指针

同一个类可以创建出多个对象,而这些对象在调用成员函数时,执行的其实是同一份函数代码。既然函数体只有一份,C++ 是怎么分清当前正在操作哪一个对象的数据呢?

关键就在于 “当前对象”。当类的成员函数正在为某个对象执行时,C++ 编译器会自动在背后提供一个名为 this 的隐含指针,专门用来指向正在调用该函数的当前对象。

C++ this 指针是什么?

在 C++ 中,this 的英文原意是 “这个”,它是一个指向 “当前对象” 的指针。哪个对象正在调用成员函数,编译器就会在背后把哪个对象的地址传给 this。

比如我们执行 stu1.showName() 时,函数内部的 this 就指向 stu1;执行 stu2.showName() 时,同一个函数内部的 this 就会改为指向 stu2。

由于 this 保存的是当前对象的内存地址,而不是对象本身,这和前面学过的结构体指针是一样的。因此,我们通过 this 访问成员时必须使用箭头运算符(->),不能使用点运算符(.)。

语法:

this->成员名

说明:

this 是 C++ 在非静态成员函数内部自动提供的指针,不需要我们手动声明,直接在成员函数里使用即可。你可以把 this->name 直观理解为 “当前对象自己的 name 成员变量”。

示例 1:使用 this 访问成员

#include <iostream>
#include <string>

class Student {
private:
    std::string name;

public:
    Student(std::string n) : name(n) {}

    void showName() {
        std::cout << "当前学生:" << this->name << '\n';
    }
};

int main() {
    Student stu1("Jack");
    Student stu2("Lucy");

    stu1.showName();    // this 指向 stu1
    stu2.showName();    // this 指向 stu2
    return 0;
}

运行结果如下。

当前学生:Jack
当前学生:Lucy

分析:

在这个例子中,stu1 和 stu2 调用的是同一个 showName() 函数。第 1 次通过 stu1 调用时,this 指向 stu1,因此 this->name 读出 "Jack"。第 2 次通过 stu2 调用时,this 指向 stu2,读出的就是 "Lucy"。

当成员函数内部没有同名变量干扰时,编译器在背后会自动帮我们补上 this->。对于这个例子来说,下面 2 种方式是等价的。

// 方式 1(推荐)
void showName() {
    std::cout << name << '\n';
}

// 方式 2
void showName() {
    std::cout << this->name << '\n';
}

因此,小伙伴们不要因为刚学会了 this,就把所有成员变量前面都加上 this->。在没有重名冲突的场景下,直接写成员名更简洁直观。

另外还要注意,只有类的非静态成员函数内部才有 this 指针。普通的全局函数没有 this 指针,后面学习静态成员时我们还会看到,静态成员函数内部同样没有 this 指针。

注意 this 指针是由编译器自动维护的,我们不能在成员函数的参数列表里手动声明 this。同时,this 的指向也是固定的,不能像 this = &other; 这样给它重新赋值让它改指别的对象,否则会直接编译报错。

区分同名参数与成员变量

既然平时直接写成员名就行,那什么时候才必须把 this-> 明确写出来呢?最常见的情况就是:成员函数的 “参数名” 与类的 “成员变量名” 完全相同时。

假设 Student 类里有一个成员变量叫 name,而在编写 setName() 函数时,为了起名直观,我们把形参名也取成了 name。如果直接写成下面这样就会出错:

void setName(std::string name) {
    name = name;    // 错误:参数赋给了自己
}

按照变量作用域的就近原则,函数的形参属于局部变量。当它与成员变量同名时,我们在函数体内部直接写 name,指代的都是距离最近的形参 name。因此 name = name; 只是把形参自己赋值给了自己,对象的成员变量根本没有任何改变。

这时我们只要在等号左边加上 this->,写成 this->name = name; 就可以了。等号左边的 this->name 明确表示 “当前对象的成员变量”,等号右边的 name 表示 “传进来的函数参数”,两边就彻底分清楚了。请看下面例子。

示例 2:区分同名参数与成员

#include <iostream>
#include <string>

class Student {
private:
    std::string name;
    int age;

public:
    Student(std::string name, int age) {
        this->name = name;    // 给成员变量 name 赋值
        this->age = age;      // 给成员变量 age 赋值
    }

    void setName(std::string name) {
        this->name = name;    // 给成员变量 name 赋值
    }

    void showInfo() {
        std::cout << name << ",年龄:" << age << '\n';
    }
};

int main() {
    Student stu("Jack", 18);
    stu.showInfo();

    stu.setName("Tony");
    stu.showInfo();
    return 0;
}

运行结果如下。

Jack,年龄:18
Tony,年龄:18

分析:

this->name = name; 这句代码中,等号左边的 this->name 表示当前对象的成员变量,等号右边的 name 表示传进来的函数参数。

借助 this->,构造函数和 setName() 的参数名都可以直接取为 name 和 age,不用为了避开重名特意改成 newName。而在 showInfo() 中没有重名参数干扰,我们直接写 name 和 age 即可。

成员变量与参数的命名习惯

在实际项目开发中,为了避免类的成员变量与函数参数重名,除了使用 this->name = name; 区分之外,很多开发团队还会通过统一的命名约定从源头上避开重名。

常见的做法是给成员变量加上前缀 m_(如 m_name、m_age),其中 m 是 “member(成员)” 的缩写;也有团队习惯在末尾加下划线(如 name_)。这样直接写 m_name = name; 就能一眼分清,连 this-> 都可以省掉。

返回 *this 实现链式调用

平时我们使用 std::cout << a << b; 输出数据时,可以在一行代码里把多个 << 连起来写。在类中,如果我们希望成员函数也能像 counter.add(10).sub(3) 这样连续调用(称为 “链式调用”),就需要让函数在末尾返回 “当前对象本身”。

前面学习指针时我们知道,对指针加上星号(*)解引用,拿到的就是该地址对应的变量本身。既然 this 保存的是当前对象的地址,那么 *this 代表的就是当前对象自己。

只要我们把成员函数的返回值类型设为当前类的引用(即 类名&),并在函数末尾写上 return *this;,每次调用完函数都会把当前对象本身返回出来,后面自然就能接着调用下一个成员函数了。下面我们来看一个例子。

示例 3:实现成员函数链式调用

#include <iostream>

class Counter {
private:
    int value;

public:
    Counter(int v) : value(v) {}

    Counter& add(int n) {
        value = value + n;
        return *this;    // 返回当前对象的引用
    }

    Counter& sub(int n) {
        value = value - n;
        return *this;    // 返回当前对象的引用
    }

    void showValue() {
        std::cout << "当前计数值:" << value << '\n';
    }
};

int main() {
    Counter counter(10);

    // 链式调用:先加 10,再减 3
    counter.add(10).sub(3);
    counter.showValue();
    return 0;
}

运行结果如下。

当前计数值:17

分析:

为什么 counter.add(10).sub(3); 可以连着写呢?我们把它的执行过程拆成以下 2 步来看就清楚了:

  • 第 1 步:先执行左半边的 counter.add(10),此时 counter 内部的 value 从 10 变成 20,并且函数末尾通过 return *this; 把 counter 对象本身作为引用返回了出来。
  • 第 2 步:既然 counter.add(10) 返回的结果就是 counter 自己,整行代码就相当于接着执行 counter.sub(3),于是 value 在 20 的基础上再减去 3,最终变成了 17。

也就是说,对于这个例子来说,上面那行链式调用代码等价于下面代码。

counter.add(10);
counter.sub(3);

这里大家要特别留意返回值类型中的引用符号(&):add() 和 sub() 的返回值类型必须写成 Counter&,这样返回的才是原来的 counter 对象本身。下面这种漏掉 &、按值返回 Counter 的写法就是错误的。

Counter add(int n) {
    value = value + n;
    return *this;
}

需要注意的是,上面这种错误写法在编译时并不会报错,但会产生隐蔽的逻辑问题。

按值返回时,第 1 次调用 counter.add(10) 虽然把原对象 counter 改成了 20,但返回的却是一个复制出来的临时新对象。紧接着的 .sub(3) 实际上减在了那个临时对象上,原来的 counter 最终只会停留在 20 而不是 17。

上一篇:

下一篇:

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号