刚刚考完研,将近5个月没正式编写过工程式项目代码,有点生疏,复用质量不高,比较多Bug,只好捧书从头学起。本篇是《C++ Primer》和《剑指Offer 名企面试官精讲典型编程题》的学习笔记。
面试题:面试官递给应聘者一张有如下代码的A4 打印纸要求分析编译运行结果,并提供3 个选项:A、编译错误;B、编译成功,运行时程序崩溃;C、编译运行正常,输出10。
class A { private: int value; public: A(int n) { value = n; } A(A other) { value = other.value; } void Print() { std::cout << value << std::endl; } }; int _tmain(int argc, _TCHAR* argv[]) { A a = 10; A b = a; b.Print(); return 0; }
对象在如下几种情况下会被拷贝,如我们初始化变量,以及以值得方式传递或返回一个对象等。在上诉代码中,复制构造函数A(A other)传入的参数other 是A的一个实例,属于传值参数,会调用复制构造函数,这样在复制构造函数内调用复制构造函数,就会形成永无休止的递归调用而导致栈溢出。因此,C++的标准不允许复制构造函数传值参数,在Visual Studio和GCC中,都将编译出错。
正确做法应该改为A(const A& other),即传入的参数的类型声明为常量引用。
引用为对象起的另外一个名字,实质仍是对象本身。如下例:
int ival = 1024; int &refVal = ival; // refVal 指向 ival (是ival的另外一个名字) int &refVal2; // 报错:引用必须被初始化
一般在初始化变量时,初始值会被拷贝到新建的对象中。然而定义引用时,程序把引用和它的初值绑定在一起,而不是将初始值拷贝给引用。因无法令引用重新绑定到另外一个对象,因此引用必须初始化。
引用即别名,它只是为一个已经存在的对象所起的另外一个名字。
同时,我们在函数内不希望改变传入的实例的状态,因此应该为传入的引用参数加上const 关键字。有时我们希望定义这样一种变量,它的值不能被改变,就可以用关键字const 对变量加以限定,这样就把该变量定义成常量了。
我们应当尽量使用常量引用。把函数不会改变的形参定义成普通的引用是一种比较常见的错误,这么做带给函数的编写者一种误导,即函数可以修改它的实参的值;此外,使用引用而非常量引用也会极大地限制函数所能接受的实参类型(const 对象、字面值或者需要类型转换的对象无法传递给普通的引用形参,具体参考《C++ Primer》P190-193。
在函数中把传入的参数类型声明为常量引用。如果传入的参数不是引用而是实例,那么从形参到实参会调用一次复制构造函数,把参数声明为引用可以避免这样的无谓消耗,提高代码的效率。
数组不能拷贝,所以我们无法以值传递的方式使用数组参数,当我们为函数传递一个数组时,实际上传递的是指向数组首元素的指针。以下三个函数的调用是等价的:
void Print(const int*); void Print(const int[]); void Print(const int[10]); // 每个函数的唯一形参都是const int*类型的
通常情况下,使用取地址符&来获取指向某个对象的指针,数组的元素也是对象,因此对数组的元素使用取地址符就能得到指向该元素的指针:
string nums[] = {"one", "two", "three"}; string *p = &nums[0]; // p指向nums的第一个元素
另外,编译器会自动地将数组名字替换为一个指向数组首元素的指针:
string *p2 = nums; // 等价于p2 = &num[0]
使用指针可以拥有更多功能,例如指针也是迭代器,允许使用递增运算符将指向数组元素的指针向前移动到下一个位置上:
int arr[] = {0, 1, 2}; int *p = arr; p++;
使用标准库函数begin 和end 来遍历数组,begin 函数返回数组首元素的指针,end 函数返回指向数组尾元素下一位置的指针,定义在iterator头文件中:
int *pbeg = begin(arr), *pend = end(arr); while(pbeg != pend && *pbeg >=0) ++pbeg;