11.1 C++中的指针
c和c++指针的最重要区别在于c++是一种类型要求更强的语言。在c中,void* 类型的指针可以随意地指向其他类型的指针,而c++中必须显示地使用类型转换通知编译器和读者,这算是它比较严谨的地方。
11.2 C++中的引用
<span style="font-size:14px;">#include <iostream>
using namespace std;
int y;
int& r = y;//r is a reference of y
const int& q = 12;// q is a reference of a block of mem
// which is of size of a integer with content 12
int x = 0;
int& a = x;//a is a reference of x
int main(int argc, char const *argv[])
{
cout << "x = " << x << ", a = " << a << endl;
a++;
cout << "x = " << x << ", a = " << a << endl;
return 0;
}</span>
上述代码段说明了引用的创建和使用规则:
1) 当引用被创建时,它必须被初始化(比较而言,指针可以在任何时候初始化,只要保证在使用它之前不为空即可)
2) 一旦一个引用被初始化为指向一个对象,它就不能改变另一个对象的引用了。
3) 不可能有NULL引用。必须确保引用是和一块合法的存储单元关联
11.2.1 函数中的引用
最经常看见引用的地方是在函数参数和返回值中。
引用作为函数参数:任何函数内的改动将对函数外的参数产生改变;
返回一个引用:就像从函数中返回一个指针一样对待,必须知道引用关联哪个内存区域,否则将不知道指向哪一个内存
1) 常量引用:用来确保被引用的参数不被函数改变
<span style="font-size:14px;">void f(int& x) {x = 2;}
void g(const int) {}
int main()
{
// f(1); //Error:
g(1);
return 0;
}</span>
f(1)调用将会出现编译错误。因为编译器首先会建立一个引用,并将其初始化为1,再为其产生一个地址和引用捆绑在一起。而存储的内容必须时常量,不能让引用的改变影响到存储空间里的值。如上,f()想把引用所绑定的地址的内容改变为2,但是我们传递的时候,1是一个常量,它并不是变量,不能修改。所以编译器会防止这种错误的发生。
2) 指针引用:如果想改变一个指针本身而不是它所指向的内容,在c语言里通常要传递指针的地址,然后在函数中用取值的方式来修改指针所指向的地址。而在c++中,则不必这样做了。
<span style="font-size:14px;">#include <iostream>
using namespace std;
void increment(int*& i) {i++;}
int main()
{
int* i = 0;
cout << "i = " << i << endl;
increment(i);
cout << "i = " << i << endl;
return 0;
}</span>
i++;后指针本身增加了,不是它原来所指向的内容增加了。
11.3 拷贝构造函数
引出拷贝构造函数的概念:为什么需要拷贝构造函数?普通的构造函数不能做到吗?
先把答案放在这里:有时我们可能需要从一个现有的对象出发构造一个新的对象。
你可能会想,不能用值传递的方式来构造一个新的对象吗?如同对于类A, h1是一个已有的对象,h2待创建,那么使用h1 = f(h2); 这样的方式能否构造一个对象?
我们先来看一段代码
<span style="font-size:14px;"><span style="font-size:14px;">//c11: HowMany.cpp
#include <fstream>
#include <string>
using namespace std;
ofstream out("HowMany.out");
class HowMany{
static int objectCount;
public:
HowMany()
{
objectCount++;
}
static void print(const string& msg ="")
{
if(msg.size() != 0)
{
out << msg << ": ";
}
out << "objectCount = " << objectCount << endl;
}
~HowMany()
{
objectCount--;
print("~HowMany()");
}
};
int HowMany::objectCount = 0;
HowMany f(HowMany x)
{
x.print("x argument inside f()");
return x;
}
int main()
{
HowMany h;
HowMany::print("after construction of h");
HowMany h2 = f(h);
HowMany::print("after call to f()");
return 0;
}</span></span>
HowMany类有一个类的静态数据成员,用于记录有多少个类的实例被创建了。objectCount在一个对象的构造函数被调用时加1,在析构函数调用时减1。print函数打印出它的值,看起来,程序运行的结果应该是,当h2被创建后,objectCount的值应该为2。实际上打印的结果如下:
after construction of h: objectCount = 1 1
x argument inside f(): objectCount = 1 2
~HowMany(): objectCount = 0 3
after call to f(): objectCount = 0 4
~HowMany(): objectCount = -1 5
~HowMany(): objectCount = -2 6
h对象生成以后,对象数为1,这是对的。然后可以看到在打印after call to f()之前竟然打印了析构函数!问题出在调用f()时, 它接收一个HowMany对象的值,并用位拷贝的方式将对象h的内容赋值到局部对象x中。由于局部对象x存在与进程的栈区,因此当程序即将离开调用的函数f()时,它即将被销毁,因此会调用析构函数。因此出现了第3行的打印,此时objectCount减1,为0了。
然而我们也并没有看到h2的构造函数被调用。这是因为,h2对象的创建也是用位拷贝的方式产生的,在f()返回时,先将临时对象x的内容复制到h2的地址中去(函数调用时会把返回地址也进行压栈,所以知道复制的目的地址)。h2的构造函数没有被调用,这是合理的。
要理解这里的描述,请先了解函数调用时,函数调用栈的基本原理。
最后在主函数退出时,h和h2对象被销毁,再次调用了两个析构函数,因此objectCount的值最终为-1;
出现上述问题的原因在于:当使用值传递去传递一个对象时,编译器默认我们想使用位拷贝的方式来创建对象。(然而这对于其他非结构,非类的内建类型的数据类型来说,是可以正常运行的)
===>解决办法:如果设计了拷贝构造函数,从现有的对象创建新对象的时候,编译器将不再使用位拷贝,而总是调用我们的拷贝构造函数。
现在来解决上述问题,重写代码:
<span style="font-size:14px;">//c11: HowMany.cpp
#include <fstream>
#include <string>
using namespace std;
ofstream out("HowMany2.out");
class HowMany{
static int objectCount;
public:
HowMany()
{
objectCount++;
}
HowMany(const HowMany& x)
{
objectCount++;
print("copy construction HowMany2");
}
static void print(const string& msg ="")
{
if(msg.size() != 0)
{
out << msg << ": ";
}
out << "objectCount = " << objectCount << endl;
}
~HowMany()
{
objectCount--;
print("~HowMany()");
}
};
int HowMany::objectCount = 0;
HowMany f(HowMany x)
{
x.print("x argument inside f()");
return x;
}
int main()
{
HowMany h;
HowMany::print("after construction of h");
HowMany h2 = f(h);
HowMany::print("after call to f()");
return 0;
}</span>
这份代码只比上面一份多加了一个函数HowMany(const HowMany& h) {},但是打印如下:
after construction of h: objectCount = 1
copy construction HowMany2: objectCount = 2
x argument inside f(): objectCount = 2
copy construction HowMany2: objectCount = 3
~HowMany(): objectCount = 2
after call to f(): objectCount = 2
~HowMany(): objectCount = 1
~HowMany(): objectCount = 0
现在可以很好地解释了。创建对象h时调用了一次构造函数,对象数为1;使用引用传递时,又调用了一次构造函数,对象数为2,它是一个局部对象,离开函数后将被销毁(等一下就会看到);函数f()返回之前,又调用了一次构造函数,生成了h2对象,然后马上局部对象x调用析构函数。可以看到在f()调用完成之后,对象数确实变为了2。也就是说,当一个类指定了拷贝构造函数之后,函数调用中与对象有关的位拷贝的操作都换成了调用构造函数!
一个类X的拷贝构造函数的形式通常为: X(const X& x) {} 指定为常引用,是因为肯定不想改变原来的对象。
11.3.3 默认拷贝构造函数
即使我们不指定拷贝构造函数,也能达到上述要求。因为编译器会采取更聪明的办法自动生成默认拷贝函构造数。看下面的例子:
<span style="font-size:18px;">#include <iostream>
#include <string>
using namespace std;
class WithCC
{
public:
WithCC() {}
WithCC(const WithCC&)
{
cout << "WithCC(WithCC&)" << endl;
}
};
class WoCC
{
string id;
public:
WoCC(const string& ident = ""): id(ident) {}
void print(const string& msg = "") const
{
if(msg.size() != 0)
{
cout << msg << ": ";
}
cout << id << endl;
}
};
class Composite
{
WithCC withcc;
WoCC wocc;
public:
Composite(): wocc("Composite()") {}
void print(const string& msg = "") const
{
wocc.print(msg);
}
};
int main(void)
{
Composite c;
c.print("Contents of c");
cout << "Calling Composite copy-constructor" << endl;
Composite c2 = c;
c2.print("Contents of c2");
return 0;
}</span>
打印如下:
Contents of c: Composite()
Calling Composite copy-constructor
WithCC(WithCC&)
Contents of c2: Composite()
对象withcc和wocc都是类Compsite中的成员,因此在Compsite类的构造函数被调用时,它们两个的构造函数也会被调用。withcc有我们自己写好的拷贝构造函数,而wocc没有。我们来看当主函数中调用了Compsite c2 = c; 时发生了什么。
之前创建c的时候,wocc也被创建,当调用c.print的时候,用字符串为wocc中的string类型变量id赋值了。这可以从打印中看出。
现在调用c2 = c; 首先调用WithCC的拷贝构造函数来创建新的withcc成员,然而Wocc并没有指定拷贝构造函数,于是这时候编译器默认的拷贝构造函数就起作用了,但它仍然只执行“位拷贝”!
11.4 指向成员的指针
怎么样只用一个指针而不是每次都通过对象或对象指针来访问一个类中的变量呢?
答案:指向成员的指针。
定义:int ObjectClass::*ptrToMember; //这样就定义了一个指向类ObjectClass中整型变量的指针
初始化:ptrToMember = &ObjectClass::a;// 这样就使ptr指向了类中的一个整型变量。
访问:objectPtr->*ptrToMember = 1; //使用当引用的时一个对象指针时,使用语法 ->* 来访问该变量。
object.*ptrToMember = 2; //当引用的是一个对象实例时,使用语法 .* 来访问该变量。
类似的,可以定义指向成员函数的指针
int (ObjectClass::*fp)(int); //定义了一个函数指针,它有一个整型参数,返回一个整形值,但作用域运算符将其限定在了ObjectClass的范围内,而不能指向其他的类中或全局函数。看例子:
#include <iostream>
using namespace std;
class Widget;
typedef int (Widget::*gWptr)(int) const;
class Widget{
void f(int) const {cout << "Widget::f()\n";}
void g(int) const {cout << "Widget::g()\n";}
void h(int) const {cout << "Widget::h()\n";}
void i(int) const {cout << "Widget::i()\n";}
enum {cnt = 4};
gWptr fptr[cnt];
public:
Widget()
{
fptr[0] = (gWptr)&Widget::f;
fptr[1] = (gWptr)&Widget::g;
fptr[2] = (gWptr)&Widget::h;
fptr[3] = (gWptr)&Widget::i;
}
void select(int i, int j)
{
if(i < 0 || i >= cnt)
return;
(this->*fptr[i])(j);
}
int count()
{
return cnt;
}
};
int main(void)
{
Widget w;
for(int i = 0; i < w.count(); i++)
{
w.select(i, 47);
}
return 0;
}
使用typedef语法定义了一个函数指针类型,然后在类Widget里面定义了一个该类型的数组,select函数根据传入的参数来决定应该调用哪一个函数,而这都通过指向成员函数的指针完成,它提供了一种更灵活的编程方式。