目录
1、引用
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空 间,它和它引用的变量共用同一块内存空间。
类型& 引用变量名(对象名) = 引用实体;
注意:引用类型必须和引用实体是同种类型的。
1.1 引用特性
引用在定义时必须初始化。
一个变量可以有多个引用
引用一旦引用一个实体,则不能引用其他实体。
int a = 0; //int& b; 引用必须初始化 int& c = a; //引用定义完后 无法改变指向 即c一直是a的引用 int d = 1; c = d; // d赋值给c //一个对象可以有多个别名,可以别名继续别名 int& e = a; int& f = a;
1.2 常引用
如果程序员的意图是让函数使用传递给它的信息,而不对这些信息进行修改,同时又想使用引用,则应使用常量引用。例如double recube(const double &ra)
。这样做后,当编译器发现代码修改了ra的值,将生成错误消息。
//常引用 void t05() { //权限不能放大 const int a = 10;//const 修饰的变量不能修改 //int& b = a; // 该语句编译时会出错,a为常量 const int& b = a; //权限可以缩小 int c = 20; const int& d = c;//d只能使用,不能修改 const int& e = 10;//权限不变化可以使用 //int& e = 10;//10是常量 使用此语句权限放大。 int i = 1; double j = i;//不同类型,i不直接给j,而是先把i给一个临时变量 临时变量给j //double& rj = i; //类型不同 无法引用 const double& rj = i; // 即rj引用的是临时变量,而临时变量具有常性 int ii = 1000; char ch = ii;//发生整型截断 }
void swapr(int& a, int& b) { int t = a; a = b; b = a; } int main() { int a = 3, b = 5; long la = 3l, lb = 5l; swapr(a, b); swapr(la, lb);// “void swapr(int &,int &)”: 无法将参数 1 从“long”转换为“int &” //无法用 "long" 类型的值初始化 "int &" 类型的引用(非常量限定) return 0; }
在早期的c++较宽松的规则下,执行上面的操作将发生什么情况呢?
上述代码类型不匹配,因此编译器将创建两个临时int变量,将他们初始化为3和5,然后交换临时变量的内容,而la和lb保持不变。
简言之,如果接受引用参数的函数的意图是修改作为参数传递的变量,则创建临时变量将阻止这种意图的实现注1。实际上,对于形参为const引用的c++函数,如果实参不匹配,则其行为类似于按值传递,为确保原始数据不被修改,将使用临时变量来存储值。
double recube(const double& ra) { double a = ra *ra * ra; return a; } int main() { int rats = 3; double x = recube(rats + 3.0); cout << x << endl; // 输出 216 }
注意:如果函数调用的参数不是左值或与相应的const引用参数的类型不匹配,则C++将创建类型正确的匿名变量,将函数调用的参数的值传递给改匿名变量,并让参数来引用该变量。
应尽可能使用const
-
使用const可以避免无意中修改数据的编程错误。
-
使用const使函数能够处理const和非const实参,否则将只能接受非const数据。
-
使用const引用使函数能够正确生成并使用临时变量。
1.3 引用和指针的区别
在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。
引用概念上定义一个变量的别名,指针存储一个变量地址。
引用在定义时必须初始化,指针没有要求。
引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体。
没有NULL引用,但有NULL指针。
在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)。
引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小。
有多级指针,但是没有多级引用。
访问实体方式不同,指针需要显式解引用,引用编译器自己处理。
引用比指针使用起来相对更安全。 指针通过某个指针变量指向一个对象后,对它所指向的变量间接操作。程序中使用指针,程序的可读性差;而引用本身就是目标变量的别名,对引用的操作就是对目标变量的操作。
引用表面好像是传值,其本质也是传地址,只是这个工作有编译器来做。
int x = 1; int& ra = x; int y = 2; ra = y;
2、缺省
缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实 参则采用该形参的缺省值,否则使用指定的实参。
-
半缺省参数必须从右往左依次来给出,不能间隔着给。
-
参数不能在函数声明和定义中同时出现。
-
缺省值必须常量或者全局变量。
3、重载
函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数或类型或类型顺序)不同,常用来处理实现功能类似数据类型 不同的问题。
重载必须是参数列表有所不同(包括个数和类型)。
函数重载不能依靠返回值的不同来构成重载,因为调用时无法根据参数列表确定调用哪个重载函数
4、内联
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调 用建立栈帧的开销,内联函数提升程序运行的效率。
类的定义方式中,声明和定义全部放在类体中时,需注意:成员函数如果在类中定义,编译器可能会将其当成内 联函数处理。如果成员函数不在类体内定义,而在类体外定义,系统并不把它默认为内置(inline )函数,调用这些成员函数的过程和调用一般函数的过程是相同的。如果想将这些成员函数指定为内置函数,应当用inline作显式声明。
内联函数特性
inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会 用函数体替换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运行效率。
inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建 议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性。
inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。
宏的优缺点?
优点:
增强代码的复用性。
提高性能。
缺点:
不方便调试宏。(因为预编译阶段进行了替换)
导致代码可读性差,可维护性差,容易误用。
没有类型安全的检查。
C++有哪些技术替代宏?
常量定义 换用const enum
短小函数定义 换用内联函
内联函数是定义在头文件还是源文件?
内联展开是在编译时进行的,只有链接的时候源文件之间才有关系。所以内联要想跨源文件必须把实现写在头文件里。如果一个内联函数会在多个源文件中被用到,那么必须把它定义在头文件中。
内联函数的定义不一定要跟声明放在一个头文件里面:定义可以放在一个单独的头文件中,里面需要给函数定义前加上inline 关键字,原因看下面第 2.点;然后声明 放在另一个头文件中,此文件include上一个头文件。这种用法 boost里很常见:优点1. 实现跟API分离封装。优点2. 可以解决有关inline函数的循环调用问题。
5、auto
typeid(变量).name() ---- 用来打印类型
C++11中,标准委员会赋予了auto全新的含义即:auto作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。
使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto 的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型。 (auto不能做函数参数以及返回值)
auto的使用规则:
-
auto与指针和引用结合起来使用 用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须 加&。
int main() { int x = 10; auto a = &x; auto* b = &x; auto& c = x; cout << typeid(a).name() << endl; \\int * __ptr64 cout << typeid(b).name() << endl;\\int * __ptr64 cout << typeid(c).name() << endl;\\int *a = 20; *b = 30; c = 40; return 0; }
-
在同一行定义多个变量。当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
auto a = 1, b = 2; auto c = 3, d = 4.0;// 该行代码会编译失败,因为c和d的初始化表达式类型不同
不能使用auto的情况:
-
auto不能作为函数的参数。
-
auto不能用来声明数组。
-
auto会与lambda表达式配合使用。
注意:auto不能声明数组
6、nullptr
NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:
define NULL 0;
NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。
注意:
在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入 的。
在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。
7、const
const int *p; int *const p;
const在*左边,则指针指向的变量的值不可以直接通过指针改变(可以通过其他方法改变0)。
const在*右边,则指针的指向不可变化。
简称为”左定值,右定向“。
简而言之,const int* p 中的 const 修饰的是指向的内容,而 int* const p 中的 const 修饰的是指针本身。
const float g_earth = 9.80; const float* pe = &g_earth;//valid const float g_moon = 1.63; float* pm = &g_moon; //invalid
第一种情况,我们既不能使用pe修改值,也不能使用g_earth修改值。但第二种情况,将g_moon的地址赋给pm,则可以使用pm修改g_moon的值,这使得g_moon的const状态很荒谬。 因此,C++中禁止将const的地址赋值给非const指针。
如果指针指向指针,则情况更加复杂。假如涉及的是一级间接关系,则将非const指针赋给const指针是可以的。
int age = 39; int *pd = &age; const int *pt = pd;
注意:如果数据类型本身并不是指针,则可以将const数据或非const数据的地址赋给指向const的指针。但只能将非const数据的地址赋值给非const指针,不能将const数据的地址赋值给非const指针。
[注1] c++ primer plus 书中P263