《c++入门经典》笔记13
第十三章 高级引用和指针
13.1按引用传递以提高效率
每次将值按对象传入函数是,都将创建该对象的一个备份。每次按值从函数返回一个对象时,也将创建其备份。
对于用户创建的大型对象,备份的代价很高。这将增加程序占用的内存量,而程序的运行速度将更慢。
在栈中,用户创建的对象的大小为其成员变量的大小之和,而每个成员变量本身也可能是用户创建的对象。从性能和内存耗用方面说,将如此大的结构传入栈中的代价非常高。
当然,还有其它代价。对于您创建的类,每次创建备份时,编译器都将调用一个特殊的构造函数:复制构造函数(拷贝构造函数)。
对于大型对象,调用这些构造函数和析构函数在速度和内存耗用方面的代价非常高。
程序清单13.1 ObjectRef.cpp
#include <iostream>
class SimpleCat {
public:
SimpleCat(); //默认构造函数
SimpleCat(SimpleCat &); //拷贝构造函数
~SimpleCat();
};
SimpleCat::SimpleCat() {
std::cout << "Simple Cat Constructor ..." << std::endl;
}
SimpleCat::SimpleCat(SimpleCat &) {
std::cout << "Simple Cat Copy Constructor ..." << std::endl;
}
SimpleCat::~SimpleCat() {
std::cout << "Simple Cat Destructor ..." << std::endl;
}
SimpleCat FunctionOne(SimpleCat theCat);
SimpleCat *FunctionTwo(SimpleCat *theCat);
int main() {
std::cout << "Making a Cat ..." << std::endl;
SimpleCat simpleCat;
std::cout << "Calling FunctionOne..." << std::endl;
FunctionOne(simpleCat);
std::cout << "Calling FunctionTwo..." << std::endl;
FunctionTwo(&simpleCat);
return 0;
}
SimpleCat FunctionOne(SimpleCat theCat) {
std::cout << "Function One. Returning ..." << std::endl;
return theCat;
}
SimpleCat *FunctionTwo(SimpleCat *theCat) {
std::cout << "Function Two. Returning ..." << std::endl;
return theCat;
}
可以看到程序中FunctionOne按值传递,FunctionTwo按址传递,FunctionOne的调用会产生一次拷贝构造,返回值是SimpleCat类型,所以也会产生一次拷贝构造,然后FunctionOne结束时,两个拷贝出来的对象(一个是调用时产生,一个是返回时产生)就会被析构函数进行析构。
调用FunctionTwo,因为参数是按引用传递,所以不会进行复制备份,也就不会调用拷贝构造函数,所以也就不会有输出。所以结果中,FunctionTwo所触发的输出语句仅有Function Two. Returning ...一句,且并未调用拷贝构造和析构函数。
13.2传递const指针
这部分其实前面十一章的笔记有提到。
虽然将指针传递给函数效率更高(比如上面的程序),但这也使得对象有被修改的风险。按值传递虽然效率较低,但是只是将复制品传递,所以并不会影响到原品,也就较按址传递多了一层保护。
要同时获得按值传递的安全性和按址传递的效率,解决的办法是传递一个指向该类常量的指针(也可为常量的常量指针,即const 类型 *const 指针变量),这样就不能修改所指向对象的值(如果是常量的常量指针,不仅不能修改所指向对象的值,也不能修改自身的值,即不能指向其它对象)
程序清单13.2 ConstPasser.cpp
#include <iostream>
class SimpleCat {
private:
int itsAge;
public:
SimpleCat();
SimpleCat(SimpleCat &);
~SimpleCat();
int getAge() const { return itsAge; }
void setAge(int age) { itsAge = age; }
};
SimpleCat::SimpleCat() {
std::cout << "Simple Cat Constructor ..." << std::endl;
itsAge = 1;
}
SimpleCat::SimpleCat(SimpleCat &) {
std::cout << "Simple Cat Copy Constructor ..." << std::endl;
}
SimpleCat::~SimpleCat() {
std::cout << "Simple Cat Destructor ..." << std::endl;
}
const SimpleCat *const
FunctionTwo(const SimpleCat *const simpleCat); //指向常量的常量指针
int main() {
std::cout << "Making a cat ..." << std::endl;
SimpleCat simpleCat;
std::cout << "simpleCat is now " << simpleCat.getAge() << " years old"
<< std::endl;
int age = 5;
simpleCat.setAge(age);
std::cout << "simpleCat is now " << simpleCat.getAge() << " years old"
<< std::endl;
std::cout << "Calling FunctionTwo..." << std::endl;
FunctionTwo(&simpleCat);
std::cout << "simpleCat is now " << simpleCat.getAge() << " years old"
<< std::endl;
return 0;
}
const SimpleCat *const FunctionTwo(const SimpleCat *const simpleCat) {
std::cout << "Function Two. Returning ..." << std::endl;
std::cout << "simpleCat is now " << simpleCat->getAge() << " years old"
<< std::endl;
// simpleCat->setAge(9);//报错,无法对const类型对象进行修改
return simpleCat;
}
如果将注释掉的代码去掉注释,程序则无法通过,所以可见将参数设置为常量的指针或者是常量的常量指针这种方式兼顾了址传递的高效与值传递的安全。
13.3作为指针替代品的引用
将上一个程序重写,使用引用而非指针。
程序清单13.3 RefPasser.cpp
#include <iostream>
class SimpleCat {
private:
int itsAge;
public:
SimpleCat();
SimpleCat(SimpleCat &);
~SimpleCat();
int getAge() const { return itsAge; }
void setAge(int age) { itsAge = age; }
};
SimpleCat::SimpleCat() {
std::cout << "Simple Cat Constructor ..." << std::endl;
itsAge = 1;
}
SimpleCat::SimpleCat(SimpleCat &) {
std::cout << "Simple Cat Copy Constructor ..." << std::endl;
}
SimpleCat::~SimpleCat() {
std::cout << "Simple Cat Destructor ..." << std::endl;
}
const SimpleCat &FunctionTwo(const SimpleCat &simpleCat); //指向常量的常量指针
int main() {
std::cout << "Making a cat ..." << std::endl;
SimpleCat simpleCat;
std::cout << "simpleCat is now " << simpleCat.getAge() << " years old"
<< std::endl;
int age = 5;
simpleCat.setAge(age);
std::cout << "simpleCat is now " << simpleCat.getAge() << " years old"
<< std::endl;
std::cout << "Calling FunctionTwo..." << std::endl;
FunctionTwo(simpleCat);
std::cout << "simpleCat is now " << simpleCat.getAge() << " years old"
<< std::endl;
return 0;
}
const SimpleCat &FunctionTwo(const SimpleCat &simpleCat) {
std::cout << "Function Two. Returning ..." << std::endl;
std::cout << "simpleCat is now " << simpleCat.getAge() << " years old"
<< std::endl;
// simpleCat.setAge(9);//报错,无法对const类型对象进行修改
return simpleCat;
}
可见,改用引用效率和代价较指针没变,但是使用起来较指针简单。
13.4什么情况下使用引用以及什么情况下使用指针
一般而言,c++程序员更喜欢使用引用而不是指针,因为他们更清晰,使用起来更容易。然而,引用不能重新赋值,如果需要依次指向不同的对象,就必须使用指针。引用不能为NULL,因此如果要指向的对象可能为NULL,就必须使用指针,而不能使用引用。如果要从堆中分配动态内存,就必须使用指针。
13.5指向对象的引用不在作用域内
c++程序员学会按引用传递后,常常会疯狂地使用这种方式。然而,过犹不及,别忘了,引用始终是另一个对象的别名,如果将引用传入或传出函数,务必自问:它是哪个对象的别名?当我使用引用时,这个对象还存在吗?
程序清单13.4 ReturnRef.cpp
#include <iostream>
class SimpleCat {
private:
int itsAge;
int itsWeight;
public:
SimpleCat(int age, int weight);
~SimpleCat(){};
int getAge() { return itsAge; }
int getWeight() { return itsWeight; }
};
SimpleCat::SimpleCat(int age, int weight) : itsAge(age), itsWeight(weight) {}
SimpleCat &TheFunction();
int main() {
SimpleCat &rCat = TheFunction();
int age = rCat.getAge();
std::cout << "rCat is " << age << " years old!" << std::endl;
return 0;
}
SimpleCat &TheFunction() {
SimpleCat simpleCat(5, 9);
return simpleCat;
}
这个程序一般编译器是会报错的,因为TheFunction()函数返回的对象引用是在TheFunction()函数内创建的,但是TheFunction()返回时,创建的这个对象是已经被销毁了的,也就是返回的引用指向了一个不存在的对象(空引用被禁止),从而被编译器禁止该程序运行。
13.6返回指向堆中对象的引用
你是否认为:如果TheFunction()函数在堆中创建对象,这样,返回的时候这个对象就依然存在,也就自然解决了上面的空引用问题。
这种方法的问题在于,使用完该对象后,如何释放为它分配的内存?
程序清单13.5 Leak.cpp
#include <iostream>
class SimpleCat {
private:
int itsAge;
int itsWeight;
public:
SimpleCat(int age, int weight);
~SimpleCat(){};
int getAge() { return itsAge; }
int getWeight() { return itsWeight; }
};
SimpleCat::SimpleCat(int age, int weight) : itsAge(age), itsWeight(weight) {}
SimpleCat &TheFunction();
int main() {
SimpleCat &rCat = TheFunction();
int age = rCat.getAge();
std::cout << "rCat is " << age << " years old!" << std::endl;
std::cout << "&rCat is " << &rCat << std::endl;
SimpleCat *pCat = &rCat;
delete pCat; //原对象被释放了,这时候rCat不就成了空引用了吗?
return 0;
}
SimpleCat &TheFunction() {
SimpleCat *simpleCat = new SimpleCat(5, 9);
std::cout << "SimpleCat: " << simpleCat << std::endl;
return *simpleCat;
}
不能对引用调用delete,一种聪明的解决方案是创建一个指针并将其指向该引用的对象的地址,这个时候调用delete也就能释放分配的内存,但是往后引用名不就成了空引用吗?虽然编译器检测不到且能正常运行,所以这就埋了一个雷,指不定什么时候炸了。(正如之前指出,引用必须始终是一个实际存在的对象的别名。如果它指向的是空对象,那么程序仍是非法的)
对于这种问题,实际上有两种解决方案。一种是返回一个指针,这样可以在使用完该指针之后将其删除。为此,需要将返回值类型声明为指针而非引用,并返回该指针。
另一种更好的解决方案是:在发出调用的函数中声明对象,然后将其按引用传递给TheFunction()。这种方法的优点是,分配内存的函数(发出调用的函数)也负责释放内存。
13.7谁拥有指针
程序在堆中分配的内存时将返回一个指针。必须一直让某个指针指向这块内存,因为如果指针丢失(也就是没有指针指向它了),便无法释放该内存,进而导致内存泄露。
在函数之间传递内存块时,其中一个函数“拥有”指针。通常,使用引用传递内存块中的值,而分配内存块的函数将负责释放它,但这是一个大致规则,并非不可打破。
然而,让一个函数分配内存,而另一个函数释放很危险。在谁拥有指针方面不明确可能会导致两个问题:忘记删除指针或重复删除。无论哪种情况,都会给程序带来严重的问题。编写函数时,让其负责释放自己分配的内存是更安全的做法。