870920 Menu

SwingCoder之C++备忘录·20

引用和指针

“指针”暗含了两个概念:它本身的值(内存地址),它所“指向”的值(由指针的类型来确定)。指针与引用的本质是对象或变量的内存地址,两者的异同:

 引用必须代表一个已经存在的值(对象),一旦初始化,就无法再代表其他对象。不存在空引用。
 引用所代表的值(对象)可以是无名的,比如数组元素:int& a = array[i]; 指针也可以。
 引用比指针安全,它不具备对象的所有权,只是单纯的指代。即:只能读或写,不能被销毁
 右值引用可以是表达式或函数返回值所产生的临时对象,进而实现内存数据的“无损转移”功能。
 非const指针可以指向类型相同或匹配的任意值(对象),可以指向空值(空指针,而非0)
 非const引用和指向非const对象的指针,皆可改变所代表或指向的值(对象)
 引用和指针均不可以代表或指向字面常量值。但const引用可以代表字面常量:
float& f = 12.3f; // 错误!
const float& f = 12.3f; // 可以!

“指针”这个名称和说法非常不严谨,应该称之为:“内存地址变量”,简称“地址变量”。这种变量本身是32位或64位int数据,不管它“代表”或“指向”什么类型的对象,它本身永远只是一个整型变量或常量。它所保存的值(其自身内存中存储的数据内容)只是内存地址,而非实际有效的数据。常说的X类型的指针,可准确的称为“X类型的地址变量”,而XX指针,则称为“XX类的某个对象的地址”(该对象有可能位于堆中,也有可能位于栈中,或者全局区中)。

XXX* 指针 = new XXX(); 这样的语句,并非指针指向new出来的XXX类的堆中对象,正确的说法应该是:在堆中new一个XXX类型的无名对象,该无名对象的内存起始地址赋值给同类型的指针变量。正因为声明指针时要冠上“类型*”,无非是告诉编译器和运行中的程序,有了堆对象的起始地址后,再沿着该地址前进多少个字节,这段范围内的内存空间中所保存的数据刚好就是该对象的所有数据,一点不多,一点不少(每个类型的字节数,即占用的内存数,在程序编译时就定死了的,因此,指针才可以刚好“代表”堆中的无名对象或普通对象的内存所在处)。

与之对应:delete 指针; 也并非销毁指针,正确的说法应该是:根据该指针变量所保存的内存地址,将该地址标记为“可使用”(delete运算符实际上并没有抹去和释放任何东西,只是将这块内存空间更改为“未使用”状态,以此通知操作系统,这块内存可以被再次分配使用。不显式delete,这块内存就永远处于“不可使用”的状态,无法将别的数据填到这块内存空间中,即使本程序退出也不行。这就是内存泄露的来龙去脉)。

正因为new出来的堆中对象是无名的,没法显式管理,才需要一个指针来“标识”这个对象,编码时使用标识该对象的指针几乎等同于直接使用该对象(除了“.”和“->”的区别)。这样做很灵活,威力很大,因为指针本身保存的只是地址,如果没有声明为常指针,那么它既可以随时保存这个对象的地址,也可以随时保存那个对象的地址;既可以随时保存堆中无名对象的地址,又可以随时保存栈中有名对象的地址;还可以随时成为一个空指针(nullptr),谁的地址都不保存。其灵活性经常导致在经验不足或复杂难缠等情况下很难驾驭。比如:如果指针当前正保存一个无名堆中对象的地址,该对象的地址除了这个指针之外,没有被其他任何对象所保存,那么,当该指针转而“指向”另一个对象,即改变了该指针所保存的内存地址之后,原来那个无名对象就彻底无人关照了,谁也没办法再使用它或者获悉它在内存中的位置。此时,不但没法再使用该对象,还发生了内存泄露的严重问题。

如果有两个指针“持有”同一个堆对象的地址,delete了A指针,那么此时B指针如果不重新赋值“改指”其他对象,那么它就成野指针了,对野指针进行的几乎任何操作,都可能会导致重大错误,或者程序崩溃,而这样的问题,并不一定马上就发生,很难跟踪调试,而且编译时也不能利用编译器来纠错。

指针本身,也就是地址变量本身,根本无需销毁或释放。它只是一个int型的变量,该变量编译时压入栈中,而非运行时动态分配堆内存,指针离开了所处的作用域,系统自动释放它本身所占的内存,这一点和使用普通的整型变量完全一致。

注意:指针并非总是或者只能“指向”堆内存中的对象或变量,它完全可以“指向”栈内存中的对象或变量,只需对栈对象使用一元运算符&(取址)即可获取该对象的内存地址,而此地址可以赋值给类型一致的指针变量。比如:
MyClass stackObject; // stackObject是一个栈对象
MyClass* ptr = &stackObject; // 将栈对象的内存地址赋值给指针变量ptr

同理,引用型的变量也并非总是或者只能引用栈对象,它也完全可以引用堆内存中的对象或变量,此时需对指针变量解引用,使用一元运算符“*”。比如(接上例):
MyClass& refer = *ptr; // refer引用的是ptr指针所“指向”的堆内存中的对象

 指针与对象的关系:
TheType objA, objB; // 定义两个TheType类的栈对象
// 定义一个指针变量,该变量只能保存TheType类型的对象的内存地址。
// 即:p只能指向TheType或其派生类的对象。该语句将p初始化为空指针
TheType* p = nullptr;
p = &objA; // &运算符取栈对象的内存地址,该地址赋值给指针变量p
// *运算符取指针变量的值,即*p即为p所指向的实际对象,此亦称为“解引用”。
// 此时,*p即objA对象
(*p).someFuc(…);
p = &objB; // objB的内存地址赋值给指针变量p
// 此时,*p变为objB对象,它所调用的someFunc()函数为objB的成员函数,
// 而与objA对象无任何关系
(*p).someFuc(…);
// 指针的运算。p此时保存的值为下一个内存地址(两个内存地址相距的字节数为该类的大小)
// 该地址所保存的对象未知,可能是垃圾数据,也可能是其他数据。
++p;
–p; // p所保存的值恢复为objB的所在地址
++(*p); // 对象的运算。该语句相当于objB对象进行前自增运算
// (前提是TheType类重载了前自增运算符)
// 定义一个整型指针pt,保存指针p的内存地址。pt即为指向指针的指针,此时,*(*pt)为objB对象本身。而*pt则为指针p本身
int* pt = &p; // 获取指针变量的地址,赋值给另一个指针。另一个指针即为指向指针的指针
 指针与抽象基类
尽管不能实例化抽象基类的对象,但可以声明指向抽象基类的指针。这样的指针可用来对该抽象基类的派生类对象进行多态性操作。父类指针可直接指向子类对象的内存地址,即子类指针可直接赋值给父类指针,而反之却不行(将父类指针赋值给子类指针),必须强转,比如:
// 父类指针转子类指针,也可使用static_cast<子类*>
子类* ptr = dynamic_cast<子类*>(父类指针);
父类* ptr = 子类指针; // 子类指针可以直接赋值给父类指针,编译器将隐式转换
 指针与数组
float myArray[arraySize]; // 声明一个数组
float* ptr = myArray; // 数组名代表该数组的内存起始地址,*ptr == myArray[0]
++ptr; // 此时对ptr解引用,值为myArray[1]
char charArray[] = “Hello World”; // 以’\0’为结束符的字符数组
char* ptrArray = “Hello World”; // 与上句等同,都是以’\0’为结束符的字符数组
以上两句,又可称为“C风格字串”,对两个C风格字串进行“==”比较,实际上比较的是两者的地址,而非内容。也就是说,尽管两者的内容完全一致,但“==”的结果依然为false。
 函数指针与函数回调
参见2.1.9。