隐藏的this指针
现在回想一下类实例的的调用方法,比如:test.func()
,返过来思考一下,这个func()
函数是怎么被调用的,比如,如果建立类实例的时候,从test
的内存里取出func()
这个函数,但是实际上这样会导致下图的存储方式:
很明显,这会导致函数代码的重复,对于追求效率的C++,这显然是可以优化的地方,并且优化的形式大概也能猜出来,就是合并函数,函数只在创建类的时候存储一次,类实例对其调用就行,如图所示:
所以,对于类实例调用函数test.func()
,有时候函数需要更改类实例的参数,它又是怎么知道实例的地址呢?
这就是本节要讲的内容,隐藏的this
指针。这个指针代表了类实例的地址。
具体举一个例子进行说明:
class Simple
{
private:
int m_id;
public:
Simple(int id)
{
setID(id);
}
void setID(int id) { m_id = id; }
int getID() { return m_id; }
};
int main()
{
Simple simple(1);
simple.setID(2);
std::cout << simple.getID() << '\n';
return 0;
}
对于这一行代码:
simple.setID(2);
实际运行的时候会被转化为:
setID(&simple, 2);
但是在setID
函数里面这个地址是怎么用的呢?
原类里面的setID
函数定义实际会被转化为:
void setID(Simple* const this, int id) { this->m_id = id; }
清楚地展示了类的成员变量是通过this->
进行地址调用和更改的。
链接成员函数
有时让类成员函数返回其使用的对象作为返回值有时会很有用。这样做的主要原因是允许将一系列成员函数“链接”在一起,因此可以在同一对象上调用多个成员函数!
class Calc
{
private:
int m_value;
public:
Calc() { m_value = 0; }
void add(int value) { m_value += value; }
void sub(int value) { m_value -= value; }
void mult(int value) { m_value *= value; }
int getValue() { return m_value; }
};
对于这个类,如果要实现加减乘除,则需要这样写:
#include <iostream>
int main()
{
Calc calc;
calc.add(5); // returns void
calc.sub(3); // returns void
calc.mult(4); // returns void
std::cout << calc.getValue() << '\n';
return 0;
}
但是由上面的可知,.func
之前的只需要接受类实列的地址就行了,所以可以把函数改成返回地址类型:
Calc& add(int value) { m_value += value; return *this; }
Calc& sub(int value) { m_value -= value; return *this; }
Calc& mult(int value) { m_value *= value; return *this; }
这样就可以实现这样的功能:
calc.add(5).sub(3).mult(4);
一行代码就可以搞定。这在类重载运算符时最常用。
Const 类对象和成员函数
“万物皆对象”,如果把定义好的类跟int
、float
这些等同为类型的话,就很容易理解常量类的概念。
常量类的定义及初始化,是通过构造函数初始化的:
const Date date3 { 2020, 10, 16 };
一旦通过构造函数初始化了const类对象,就不允许尝试修改该对象的成员变量,因为这会违反该对象的const-ness。这包括直接更改成员变量(如果它们是公共的)或调用设置成员变量值的成员函数。其实const类对象只能显式调用const成员函数。
但是构造函数不能标记为const。这是因为构造函数需要能够初始化其成员变量,而const构造函数则不能这样做。因此,C++不允许使用const构造函数。
如下代码就是常量类调用非常量函数会报错的错误示例。
class Something
{
public:
int m_value;
Something(): m_value(0) { }
void setValue(int value) { m_value = value; }
int getValue() { return m_value ; }
};
int main()
{
const Something something; // 定义const 类
something.m_value = 5; // compiler error: violates const
something.setValue(5); // compiler error: violates const
return 0;
}
const成员函数
const成员函数是一个成员函数保证它不会修改对象或调用任何非const成员函数(它们可能会修改对象)。
const
加的位置是函数名之后,函数结构体之前。
class Something
{
public:
int m_value;
Something(): m_value(0) { }
void resetValue() { m_value = 0; }
void setValue(int value) { m_value = value; }
int getValue() const { return m_value; } // note addition of const keyword after parameter list, but before function body
};
对于在类定义之外定义的成员函数,必须在类定义中的函数原型和函数定义上都使用const关键字:
class Something
{
public:
int m_value;
Something(): m_value(0) { }
void resetValue() { m_value = 0; }
void setValue(int value) { m_value = value; }
int getValue() const; // note addition of const keyword here
};
int Something::getValue() const // and here
{
return m_value;
}
静态类成员和类成员函数
对象与对象之间的成员变量是相互独立的。如果对象之间需要通信怎么办?或者共享一个参数怎么办?这时候就需要使用静态成员和静态函数。
① 定义及使用的一个示例:
static
关键字放在最前面
class Something
{
private:
static int s_idGenerator;
int m_id;
public:
Something() { m_id = s_idGenerator++; } // grab the next value from the id generator
int getID() const { return m_id; }
};
int Something::s_idGenerator = 1; // start our ID generator with value 1
int main()
{
Something first;
Something second;
Something third;
std::cout << first.getID() << '\n';
std::cout << second.getID() << '\n';
std::cout << third.getID() << '\n';
return 0;
}
输出:
1
2
3
②类成员必须初始化
由于静态成员和静态函数不依附于类的对象,所以他的定义跟类对象没有关系,在程序编译的时候就已经分配存储空间了,到程序结束时才释放。由于编译阶段就要分配空间,所以必须进行初始化。
上面的例子中,该行代码就是:
int Something::s_idGenerator = 1;
③调用方式
由于静态成员及函数在一开始就已经被分配好,其实性质跟全局变量非常接近,只不过是限定在了这个类的命名空间里,所以就可以把类当成一个命名空间namespace
,调用方式也一样。并且由于它不依附于类的实例化,所以即使没有类对象,也可以通过name::member
的方式调用静态成员或者函数。
④静态与非静态之间的调用关系
可以总结为这三句话:
- 静态成员函数可以直接访问其他静态成员(变量或函数),但不能访问非静态成员。这是因为非静态成员必须属于类对象,并且静态成员函数没有可使用的类对象。因为它没有
this
指针的参数。 - 类的非静态成员函数可以调用用静态成员函数,但反之不能。因为一个创建的先,一个后,所以后创建可以调用先创建的。
friend 函数和类
前面的static
静态成员和函数实现了不同的类对象之间的互相通信,但需要通信的肯定不止类对象之间,比如类与类,类与函数都有存在需要通信访问的需求,这就需要friend
功能了。
朋友函数或类是可以访问另一个类的私有成员的函数或类,就像它是该类的成员一样。这允许朋友类与另一个类密切合作,而无需使另一个类公开其私有成员(例如,通过访问功能)。
friend 定义
把一个函数的声明前面加上friend
关键字写到想访问的类里面即可。
class Accumulator
{
private:
int m_value;
public:
Accumulator() { m_value = 0; }
void add(int value) { m_value += value; }
// Make the reset() function a friend of this class
friend void reset(Accumulator &accumulator);
};
// reset() is now a friend of the Accumulator class
void reset(Accumulator &accumulator)
{
// And can access the private data of Accumulator objects
accumulator.m_value = 0;
}
int main()
{
Accumulator acc;
acc.add(5); // add 5 to the accumulator
reset(acc); // reset the accumulator to 0
return 0;
}
请注意,我们必须将一个Accumulator对象传递给reset()。这是因为reset()不是成员函数。它没有* this指针,也没有可以使用的Accumulator对象,除非给定一个指针。
多个类的共同friend
一个函数可以同时是两个类的friend
。
需要注意的是,在类Temperature
前必须声明class Humidity;
,要不然friend
函数不知道Humidity是什么。
#include <iostream>
class Humidity;
class Temperature
{
private:
int m_temp;
public:
Temperature(int temp=0) { m_temp = temp; }
friend void printWeather(const Temperature &temperature, const Humidity &humidity);
};
class Humidity
{
private:
int m_humidity;
public:
Humidity(int humidity=0) { m_humidity = humidity; }
friend void printWeather(const Temperature &temperature, const Humidity &humidity);
};
void printWeather(const Temperature &temperature, const Humidity &humidity)
{
std::cout << "The temperature is " << temperature.m_temp <<
" and the humidity is " << humidity.m_humidity << '\n';
}
int main()
{
Humidity hum(10);
Temperature temp(12);
printWeather(temp, hum);
return 0;
}
friend
类
也有可能使整个类成为另一个类的朋友。这使所有好友类别的成员都可以访问其他类别的私有成员。这是一个例子:
#include <iostream>
class Storage
{
private:
int m_nValue;
double m_dValue;
public:
Storage(int nValue, double dValue)
{
m_nValue = nValue;
m_dValue = dValue;
}
// Make the Display class a friend of Storage
friend class Display;
};
class Display
{
private:
bool m_displayIntFirst;
public:
Display(bool displayIntFirst) { m_displayIntFirst = displayIntFirst; }
void displayItem(const Storage &storage)
{
if (m_displayIntFirst)
std::cout << storage.m_nValue << " " << storage.m_dValue << '\n';
else // display double first
std::cout << storage.m_dValue << " " << storage.m_nValue << '\n';
}
};
int main()
{
Storage storage(5, 6.7);
Display display(false);
display.displayItem(storage);
return 0;
}
注意事项:
-
使用好友函数和类时要小心,因为它会使好友函数或类违反封装。如果班级的详细信息发生变化,朋友的详细信息也将被迫更改。因此,将对朋友功能和类的使用限制为最少。
-
当两个或多个类需要以亲密的方式一起工作时,或者在定义重载运算符时更常见的情况下,通常不使用Friending。
-
请注意,要使特定的成员函数成为朋友,需要首先看到成员函数类的完整定义。