书接上回,在介绍了函数重载后,我们将继续学习包括引用和函数内联等C++入门知识。
extern "C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言的进行编译,而不是C++的。
//引入extern "C" 是告诉编译器其修饰下的函数的命名修饰规则按照C语言下的命名规则进行 //即此时的函数Add在转化到符号表中后为_Add 而非 _Z3Addii extern "C" int Add(int x, int y) { return x + y; }
通过调试可以看到在编译时函数名Add转换为符号表后为_Add
而非C++命名规则中的 _Z3Addii
.
比如:tcmalloc
是google用C++实现的一个项目,他提供tcmallc()
和tcfree()
两个接口来使用,但如果是C项目就没办法使用,那么他就使用extern “C”
来解决。
引用并不会定义新的变量,而是给原来的变量起一个“别名”。因此编译器并不会为引用变量单独开辟一块内存空间,该变量与它引用的变量共用同一块内存空间。
引用的符号为&,其使用方法是 类型 + & + 引用变量名称(对象名) = 引用实体;
int main() { int a = 10; int& ra = a;//定义引用类型 cout << a << endl; cout << ra << endl; return 0; }
从图中我们可以看出变量a以及它的引用变量ra的地址都为0x0133fd04,且值均为10。
需要注意的是:引用变量的自身的类型必须和引用对象是同类型的。
1.引用在定义同时必须要初始化。这很好理解,比如在取别名的时候就需要有这个被取别名的对象。
2.一个引用实体可以有多个引用。同样的,一个对象的别名可以有很多个。
3.一旦引用变量成为了一个实体的引用,就不能再成为其他实体的引用、这也不难理解,一个别名如果同时是多个对象的别名,那么就会产生歧义,这对于编译器是严格禁止的。
int main() { int a = 10; int& ra;//该句代码编译出错 int& ra = a; int& rra = a; int b = 20; ra = b;//这句代码的意思是将b的值赋给ra也就是赋给a return 0; }
我们知道在C语言中有个关键字const,其修饰后的变量为常变量,即该变量的值不可修改,那么对于常变量的引用,又是什么样子的呢?
这个时候的引用前也需要加上const构成常引用。
const int a = 10; //int& ra = a;//该句代码编译出错,因为a为常量 const int& ra = a;
那么下面的代码是否可以编译成功呢?
int b = 20; const int& rb = b;
不难看出上述代码并没有错误,这是因为rb作为b的常引用,和b共用一块空间,只不过rb不能修改,也就是说不能通过修改rb来修改b,但是可以修改b是rb改变。
由此也可以看出引用的类型必须和引用实体的类型一致,比如下面的代码就是错误的。
char c = 'C'; int& rc = c;//编译出错,rc的类型与c的类型不一致
由于引用是引用实体的别名,因此可以通过引用来修改引用实体,而我们知道在函数传参时,如果不是通过传地址进行传参的话,此时的参数是原来变量的一份临时拷贝,即形参;我们并不能通过修改形参达到修改实参的目的,因此,通过引用传参也是一个很好的方法。
void Swap(int& ra, int& rb)//引用传参 { int tmp = ra; ra = rb; rb = tmp; }
其次,常引用在传参过程中也有作用,比如下面情形中:
//在打印栈中元素时 void printStack(const Stack& s) { //当我们想判断capacity是否为0时,不小心将==写成了= if (s.capacity = 0) { return; } }
这时如果我们调用的函数并不会修改我们的参数,那么可以用const修饰引用达到保护的作用。
引用作返回值需要我们注意一下其使用的情形。首先我们需要认识到如何使用引用作返回值,那么我们先来看看下面这段代码是否正确:
int Add(int x, int y) { int z = x + y; return z; } int main() { int a = 10; int b = 20; int& ra = Add(a, b); cout << ra << endl; }
实际上在int& ra = Add(a,b);
这句代码上有错误,这是怎么回事呢?我们知道函数调用是会建立栈帧的,而栈帧在函数调用结束后会销毁并将这块栈帧还给操作系统,那么函数中创建的变量也会被销毁;但是为什么函数的返回值还能被接收呢?这是因为系统会创建一个临时变量,函数返回值会赋给这个临时变量,同时这个临时变量又会赋给要赋给的变量;而这种临时变量具有常性,比如说上述代码中z的值赋给了一个类型为const int 的临时变量,而ra作为int类型的引用,显然是无法接收的,因此会产生错误。
既然返回值的类型为int不行,那么是否可以改为int& 作为函数的返回类型呢?
int& Add(int x, int y) { int z = x + y; return z; }
当改变函数的返回值类型后,此时函数的输出为30,结果符合预期,但是编译器给出了警告:
这时我们对代码稍作修改,结果却与我们想象的有所不同:
此时的输出结果为第二个Add函数调用后的结果,这又是为什么呢?
从上面就可以看出引用作为函数返回值的情形应该是返回值为全局变量或则static修饰的变量等出了函数不会销毁的变量,这样才不会产生越界的危险。
int& Count() { static int n = 0; n++; return n; }
因此,需要注意的是:在函数返回时,出了函数作用域,如果返回对象还未还给系统,则可以使用引用返回,如果已经还给系统了,则必须使用传值返回。
我们知道,C语言中函数有传值传址两种方式,而传值由于是形参对实参的一份临时拷贝,在实参占用的空间很大时会消耗巨大的资源从而使效率十分低下,而传址就能够很好的解决这个问题。而C++中的引用也能和指针一样使代码的效率大大提升,同时相较于指针,C++的引用会更阿红理解,毕竟并不需要通过解引用去找到原变量,因为引用本身就是原变量的别名,改变引用就是改变引用实体。
引用作为引用实体的别名,没有独立空间,与实体共用同一块空间,但是在底层实现上,引用是按照指针的方式实现的。
那么引用与指针的区别有哪些呢?
(1)引用需要初始化,而指针没有要求。
(2)引用一旦作为一个引用实体的引用,就不能再作为其他实体的引用,但指针可以修改其所指向的对象的。
(3)引用没有独立空间,而指针有。
(4)对于sizeof,引用变量的大小与类型有关,指针变量的大小与类型无关,而取决于系统。
(5)对于自加,引用加一是数值上加一,而指针加一是跳过一个类型的大小。
(6)访问实体的方式不同,指针是通过解引用访问,而引用是编译器自己处理。
(7)引用使用起来相对于指针更安全。(指针容易出现野指针和空指针等非法访问的问题)
我们知道函数调用会建立栈帧,而建立栈帧的过程有时候会很复杂,如果对于一个程序中需要频繁调用同一个函数,那么我们可以用inline这个关键字修饰要调用的函数,这就是内联函数。
对于内联函数,C++编译器会再调用内联函数的地方直接展开,减少了函数建立栈帧的开销,从而提升程序运行的效率。
相对于C++中的内联函数,C语言的宏也是可以达到不用建立函数栈帧,直接展开的目的。
对于宏,由于其在预处理阶段就展开了,因此提高了代码的复用性,同时对于反复调用的宏,也能提高代码的效率。
但是,宏的缺点也很明显,首先,由于宏在预处理阶段就展开了,因此宏是不可调试的;其次,宏会导致代码的可读性差,可维护性差,同时容易无用;此外,宏的定义并没有类型检查,相较于函数并不安全。
因此,为了避免上述问题,对于宏定义的常量,可以用const定义来替代;而对于宏定义的函数,C++也可以用内联函数来替代。
早在C++98标准中就存在了auto关键字,那时的auto用于声明变量为自动变量,拥有自动的生命周期;但是该作用是多余的,变量默认拥有自动的生命周期。
int a = 10;//自动生命周期 auto int b = 20;//自动声明周期
因此,在C++11中,auto的该用法被删除,取而代之的是:自动推断变量的类型。
int main() { int a = 10; auto b = a; auto c = 'c'; auto d = 3.0; cout << typeid(b).name() << endl;//显示变量类型 cout << typeid(c).name() << endl; cout << typeid(d).name() << endl; return 0; }
需要注意的是,auto定义的变量必须要进行初识化,这是因为auto定义的变量的类型是有编译器在编译阶段判断的,若不进行初始化,编译器无法识别变量的类型,会导致无法编译。。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型。
int main() { int a = 10; auto b = &a; auto* c = &b; auto& d = a; cout << typeid(b).name() << endl; cout << typeid(c).name() << endl; cout << typeid(d).name() << endl; return 0; }
首先一点,auto的变量声明指针类型时,auto与auto*并无区别;其次,auto声明引用时,需要在auto后加上&。
int main() { auto a = 10, b = 20; auto c = 3, d = 4.0;//编译错误,c和d的初始表达式类型不同 return 0; }
auto在同一行声明的多个变量类型需要相同,这是因为编译器只会对一个变量识别类型,并用这个推导出来的类型定义其他变量。
auto声明的变量不能作为函数的形参类型,这是因为在编译阶段编译器无法推导形参的类型。
int Count(auto n)//此处代码编译失败 { return 5; } int main() { int n = 0; Count(n); r
int main() { int arr1[] = { 1,2,3,4,5 }; auto arr2[] = { 6,7,8,9,10 }; return 0; }
C++11中引入了基于范围的for循环,for后的括号中由“:”为界分成了两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。
int main() { int arr[] = { 1,2,3,4,5,6,7,8,9,10 }; for (auto e : arr) { cout << e << " "; } cout << endl; return 0; }
与普通循环类似,范围for循环也可以用continue结束本次循环或用break来跳出整个循环。
对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。
以下代码就有问题,因为for循环的范围不确定:
void Func(int arr[]) { for (auto& e : arr) { } }
对于未初始化的指针,为了防止野指针的出现,我们通常会将指针置空;在C语言中初始化指针一般是将指针赋值为NULL,实际上,NULL是一个宏,在C语言的头文件(stddef.h)中我们可以看到:
#ifndef NULL #ifdef __cplusplus #define NULL 0 #else #define NULL ((void *)0) #endif #endif
可以看到,在C++中,NULL被定义为字面常量0,其他情况下,NULL被无指针类型(void*)的常量0,无论使用何种定义,在使用NULL作为空指针时,总会不可避免的出现一些问题:
void Test(int p) { cout << "int" << endl; } void Test(int* p) { cout << "int*" << endl; } int main() { Test(0); Test(NULL); Test(nullptr); return 0; }
在C++98中,编译器默认将NULL看成一个整型常量,在函数重载的作用下,编译器无法通过NULL来调用参数为指针类型的Test函数,这会产生歧义。如果要将其按照指针方式来使用,必须对其进行强转(void *)0。
因此,需要注意的是:
1.nullptr在C++11中是作为新关键字引入的,因此在使用其表示空指针时,无需包含头文件。
2.在C++11中,sizeof(nullptr)
与sizeof((void*)0)
大小相同。
3.为了提高代码的健壮性,后续代码中表示指针空值时最好使用nullptr。