【C++】 入门篇 从C到C++
前言在常见的C代码中我们经常能够看到开头总是有#includeiostreamusingnamespacestd;intmain(){return0;}这些是什么东西干什么用的和C语言有什么区别呢看完这一篇你就从C语言成功转入C了。一、命名空间1.1引入在C语言中我们要定义一些变量可能会出现与库中的一些函数和变量一致时会名字冲突导致我们无法进行定义比如#includestdio.h// 定义和库函数同名的全局函数voidprintf(constchar*str){printf(我的printf);}intmain(){printf(test);return0;}1.2使用命名空间但是C就可以解决C的命名冲突问题它的关键字就是namespace先说一下命名空间是如何进行定义的命名空间的定义分为3种命名空间名称及域作用限定符- 域作用限定符访问对象只在全局域中出现直接访问全局域访问对象在全局域和函数局部域同时出现就近访问原则优先访问局部使用域访问限定符时直接访问域里面的在项目中我们经常使用这种因为不同的人取的名可能会重复。在上面那个例子中我们就可以这么写。// 自定义命名空间namespaceljm{voidprintf(constchar*str){// 前面加是为了调用c库的printf::printf(我的printf%s\n,str);}}intmain(){// 调用自定义命名空间的printfljm::printf(test);return0;}使用using namespace 命名空间名称 引入我们在日常运用中经常使用部分的函数会把命名空间的所有变量函数都展开这样就可以减少我们的代码量不然每一次使用一个东西都要引入他的命令空间这也就解释了为什么很多代码中都有usingnamespacestd;这里就是把std命令空间的东西进行展开举个例子命名空间N里面有变量A,B,C,D。我们就可以通过这样来引入//这样就引入了N里的所有变量函数namespaceN{intA1;intB2;intC3;intD4;}usingnamespaceN;intmain(){printf(%d,A);printf(%d,B);printf(%d,C);printf(%d,D);return0;}- 使用using将命名空间中某个成员引入但是假设我们如果不需要N里面的所有变量只要A呢我们就可以这样namespaceN{intA1;intB2;intC3;intD4;}usingN::A;intmain(){printf(%d\n,A);return0;}二、C输入输出2.1引入学习任何语言之前我们都要学习他的输入输出。2.2使用#includeiostream// std是C标准库的命名空间名C将输入输出等定义实现都放到这个命名空间中usingnamespacestd;intmain(){inta0;cina;coutHello C!!!endl;return0;}2.3区别- 在C语言中我们的输入输出都是在stdio.h的printfscanf。 - 在C中我们的输入输出是在iostream的coutcin。 - 细心的你肯定发现了c的头文件没有.h对的我们在c包含头文件都不要.h那如果我们要c语言中的头文件呢我们就可以这样写这样就包含了c的输入输出头文件。- 在c中我们们不用像写c一样去写类型了为什么呢这个后面揭晓。2.4收尾c的输入输出更像一种流。cin会从标准输入流读到东西流到变量里。cout会从标准变量读到东西流到标准输出流里。三、缺省参数3.1 概念缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时如果没有指定实参则采用该形参的缺省值否则使用指定的实参。有点像备胎的意思了。大概这样的。voidFunc(inta0){coutaendl;}intmain(){Func();//在没有传实参是会用0Func(10);//有传实参的时候就不用用自己的。return0;}3.2分类全缺省参数voidFunc(inta10,intb20,intc30){//每个值都有缺省参数couta aendl;//输出10coutb bendl;//输出20coutc cendl;//输出30}半缺省参数voidFunc(inta,intb10,intc20){couta aendl;//a必须传值他没有缺省参数不然会报错coutb bendl;//10coutc cendl;//20}注意1. 半缺省参数必须从右往左依次来给出不能间隔着给 2. 缺省参数不能在函数声明和定义中同时出现 3. 缺省值必须是常量或者全局变量如果给一个变量编译器不知道是什么值//a.hvoidFunc(inta10);// a.cppvoidFunc(inta20){}// 注意如果声明与定义位置同时出现恰巧两个位置提供的值不同//那编译器就无法确定到底该用那个缺省值。四、 函数重载4.1引入自然语言中一个词可以有多重含义人们可以通过上下文来判断该词真实的含义即该词被重载了。在c中我们要实现add进行不同类型要取不同的名称,比较繁琐.staticinlineintadd_i(inta,intb){returnab;}doubleadd_d(doublea,doubleb){returnab;}4.2 概念是函数的一种特殊情况C允许在同一作用域中声明几个功能类似的同名函数这些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同来解决不同的问题。参数类型不同像刚才那种情况我们就可以这样写。intadd(inta,intb){returnab;}doubleadd(doublea,doubleb){returnab;}intmain(){coutadd(1,1)endl;coutadd(1.1,1.1)endl;return0;}函数调用就像这样的编译器它会自动处理- **参数类型顺序不同**voidf(inta,charb){coutf(int a,char b)endl;}voidf(charb,inta){coutf(char b, int a)endl;}- **参数个数不同**voidf(){coutf()endl;}voidf(inta){coutf(int a)endl;}4.3 C支持函数重载的原理–名字修饰在C/C中一个程序要运行起来需要经历以下几个阶段预处理→编译→汇编→链接四个阶段最终生成可执行文件。C和C基本都一样但是在链接处有点区别就导致了C支持重载实际项目通常是由多个头文件和多个源文件构成我们知道当前 a.cpp 中调用了 b.cpp 中定义的 Add 函数时编译后链接前a.o 的目标文件中没有 Add 的函数地址因为 Add 是在 b.cpp 中定义的所以 Add 的地址在 b.o 中。那么怎么办呢所以链接阶段就是专门处理这种问题链接器看到 a.o 调用 Add但是没有 Add 的地址就会到 b.o 的符号表中找 Add 的地址然后链接到一起。符号表就是一个函数对应一个所处地址。那么链接时面对 Add 函数链接器会使用哪个名字去找呢这里每个编译器都有自己的函数名修饰规则。在 Linux 下 g 的修饰规则简单易懂下面我们使用了 g 演示了这个修饰后的名字。这里发现在linux下C语言采用gcc编译完成后函数名字的修饰没有发生改变这里发现在linux下C语言采用g编译完成后函数名字的修饰发生了改变会在后面加入类型长度通过上面我们可以得出gcc的函数修饰后名字不变。而g的函数修饰后变成【_Z函数长度函数名类型首字母】。注意如果两个函数函数名和参数是一样的返回值不同是不构成重载的因为调用时编译器没办法区分,因为函数的函数名修饰规则没有包含返回值.五、引用5.1 概念引用不是新定义一个变量而是给已存在变量取了一个别名编译器不会为引用变量开辟内存空间它和它引用的变量共用同一块内存空间。有点像共享单车。我和你都可以使用这个共享单车。5.2使用类型 引用变量名(对象名) 引用实体inta0;intraa;//定义引用类型//ra和a共享一块空间谁改了另一个就改了注意引用类型必须和引用实体是同种类型的5.3引用特性引用在定义时必须初始化不然无法知道我引用的是那块空间一个变量可以有多个引用一个共享单车可以很多人骑吗一块空间可以由多个引用来管理。引用一旦引用一个实体再不能引用其他实体一个共享单车只能一个人骑我管理这块空间了就不能再去管理别的空间了。voidTestRef(){inta10;// int ra; // 该条语句编译时会出错(引用必须初始化)intraa;intrraa;printf(%p %p %p\n,a,ra,rra);}5.4使用场景做参数voidSwap(intleft,intright){inttempleft;leftright;righttemp;}做返回值intAdd(inta,intb){staticintcab;returnc;}intmain(){intretAdd(1,2);Add(3,4);coutAdd(1, 2) is :retendl;return0;}做返回值的时候我们要注意一下如果我们不加static的话他出了add这个函数的栈帧他会把空间给OS此时这个空间就不安全了你外面引用就可能出现问题。此时如果你调用一个函数压栈返回回来的c就是随机值了。总结如果出了函数作用域还在 用引用可以少开空间提高效率。5.5 权限先看一段代码voidTestConstRef(){constinta10;//int ra a; // 该语句编译时会出错a为常量constintcraa;intb100;constintcrbb;doubled12.34;//int rd d; // 该语句编译时会出错类型不同}这里我们发现ra,rd都会报错我们一个一个来分析aconst修饰只读ra可读可改cra只读b可读可改crb只读只有在引用处指针才会有权限的概念。a就像我们都是管理一块空间我是老大我对这块空间只读你引用了我还可以改了不行是权限的放大。b就像我们都是管理一块空间我是老大我对这块空间可读可改你引用了只读可以是权限的缩小。d:类型决定了我们对一块空间的访问方式我对这块空间一次访问8个字节你只访问4个字节不行5.5传值、传引用效率比较以值作为参数或者返回值类型在传参和返回期间函数不会直接传递实参或者将变量本身直接返回而是传递实参或者返回变量的一份临时的拷贝因此用值作为参数或者返回值类型效率是非常低下的尤其是当参数或者返回值类型非常大时效率就更低。写个代码验证一下#includectimestructA{inta[10000];};voidTestFunc1(A a){}voidTestFunc2(Aa){}intmain(){A a;// 以值作为函数参数size_t begin1clock();for(size_t i0;i10000;i)TestFunc1(a);size_t end1clock();// 以引用作为函数参数size_t begin2clock();for(size_t i0;i10000;i)TestFunc2(a);size_t end2clock();// 分别计算两个函数运行结束后的时间coutTestFunc1(A)-time:end1-begin1endl;coutTestFunc2(A)-time:end2-begin2endl;return0;}通过上述代码的比较发现传值和指针在作为传参以及返回值类型上效率相差很大。5.6 引用和指针的区别引用概念上定义一个变量的别名指针存储一个变量地址。在底层实现上实际是有空间的因为引用是按照指针方式来实现的。引用在对象前面例int* ba 还是取地址引用在类型后面例int rbb变成了引用引用在定义时必须初始化指针没有要求(尽量初始化)引用在初始化时引用一个实体后就不能再引用其他实体而指针可以在任何时候指向任何一个同类型实体没有NULL引用但有NULL指针在sizeof中含义不同引用结果为引用类型的大小但指针始终是地址空间所占字节个数(32位平台下占4个字节)引用自加即引用的实体增加1指针自加即指针向后偏移一个类型的大小有多级指针但是没有多级引用访问实体方式不同指针需要显式解引用引用编译器自己处理引用比指针使用起来相对更安全引用不需要assert断言。指针很容易出现空指针和野指针的问题六、内联函数6.1 概念以inline修饰的函数叫做内联函数编译时C编译器会在调用内联函数的地方展开没有函数调用建立栈帧的开销内联函数提升程序运行的效率。内联函数的出现直接取代了C语言中的宏函数。正常的函数是到调用的位置会通过地址去找这个函数的定义,我们来看汇编下的函数调用,call表示函数跳转地址如果在上述函数前增加inline关键字将其改成内联函数在编译期间编译器会用函数体替换函数调用。这里我们发现他没有函数调用了6.2 特性inline是一种以空间换时间的做法如果编译器将函数当成内联函数处理在编译阶段会用函数体替换函数调用缺陷可能会使目标文件变大优势少了调用开销提高程序运行效率。2. inline对于编译器而言只是一个建议不同编译器关于inline实现机制可能不同一般建议将函数规模较小(即函数不是很长具体没有准确的说法取决于编译器内部实现)、不是递归、且频繁调用的函数采用inline修饰否则编译器会忽略inline特性。但是合理使用可以将内联函数的缺点缩小到极小可忽略不计。编译器会帮助你合理使用。3. inline不建议声明和定义分离分离会导致链接错误。因为inline被展开就没有函数地址了链接就会找不到。6.3与宏的区别// 实现一个ADD宏函数的常见问题//#define ADD(int a, int b) return a b;//#define ADD(a, b) a b;//#define ADD(a, b) (a b)// 正确的宏实现#defineADD(a,b)((a)(b))为什么宏函数不能加分号宏函数在使用时会进行宏替换将宏名部分全部替换为宏体如果宏体有分号会被一块儿替换进去产生编译错误为什么要有外括号.为什么要有内括号优先级很错乱错误的千奇百怪,尽量在每一个变量加一个括号(防御性编程)我们可以得出以下结论不方便调试宏。因为预编译阶段进行了替换导致代码可读性差可维护性差容易误用。没有类型安全的检查 。以上的缺点我们都可以使用inline来克服。七、auto关键字(C11)7.1引入后面我们会学到迭代器类型有这种的std::mapstd::string,std::string::iterator这里我们要打特别多字所以auto应运而生7.2概念使用auto定义变量时必须对其进行初始化不然无法推导在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明而是一个类型声明时的“占位符”编译器在编译期会将auto推导出变量实际的类型。intTestAuto(){return10;}intmain(){inta10;autoba;autoca;autodTestAuto();couttypeid(b).name()endl;couttypeid(c).name()endl;couttypeid(d).name()endl;}7.3 auto的使用细则auto与指针和引用结合起来使用用auto声明指针类型时用auto和auto*没有任何区别但用auto声明引用类型时必须加intmain(){intx10;autoax;auto*bx;autocx;couttypeid(a).name()endl;couttypeid(b).name()endl;couttypeid(c).name()endl;*a20;*b30;c40;return0;}在同一行定义多个变量当在同一行声明多个变量时这些变量必须是相同的类型否则编译器将会报错因为编译器实际只对第一个类型进行推导然后用推导出来的类型定义其他变量。voidTestAuto(){autoa1,b2;//int a1,b2autoc3,d4.0;// 该行代码会编译失败因为c和d的初始化表达式类型不同}7.4 auto不能推导的场景auto不能作为函数的参数// 此处代码编译失败auto不能作为形参类型因为编译器无法对a的实际类型进行推导voidTestAuto(autoa){}auto不能直接用来声明数组voidTestAuto(){inta[]{1,2,3};autob[]{456};}7.5总结auto在实际中最常见的优势用法就是跟下面会讲到的C11提供的新式for循环还有lambda表达式等进行配合使用。八、 基于范围的for循环(C11)8.1引入在C和C11之前我们要写for都要像怎么写intmain(){intarray[]{1,2,3,4,5};for(inti0;isizeof(array)/sizeof(array[0]);i)array[i]*2;for(int*parray;parraysizeof(array)/sizeof(array[0]);p)cout*pendl;}我们发现写这种有范围的for非常麻烦因此就引入了基于范围的for循环(C11)8.2语法for循环后的括号由冒号“ ”分为两部分第一部分是范围内用于迭代的变量这里我们就回收上面讲的auto了第二部分则表示被迭代的范围。intmain(){intarray[]{1,2,3,4,5};for(autoe:array)e*2;for(autoe:array)coute ;return0;}注意与普通循环类似可以用continue来结束本次循环也可以用break来跳出整个循环。8.3 范围for的使用条件for循环迭代的范围必须是确定的对于数组而言就是数组中第一个元素和最后一个元素的范围对于类而言应该提供begin和end的方法(后面会讲到的迭代器)begin和end这两个方法定义了for循环迭代的起始和结束范围。注意以下代码就有问题因为for的范围不确定。voidTestFor(intarray[]){for(autoe:array)couteendl;//错误}迭代的对象要实现和的操作。后面具体讲九、指针空值nullptr(C11)9.1 引入在良好的C/C编程习惯中声明一个变量时最好给该变量一个合适的初始值否则可能会出现不可预料的错误比如未初始化的指针。如果一个指针没有合法的指向我们基本都是按照如下方式对其进行初始化voidTestPtr(){inta0;int*p1NULL;int*p2a;// ……}NULL实际是一个宏在传统的C头文件(stddef.h)中可以看到如下代码#ifndefNULL#ifdef__cplusplus#defineNULL0#else#defineNULL((void*)0)#endif#endif可以看到NULL可能被定义为字面常量0或者被定义为无类型指针(void*)的常量。9.2NULL的问题先看以下的这个代码voidf(int){coutf(int)endl;}voidf(int*){coutf(int*)endl;}intmain(){f(0);f(NULL);f((int*)NULL);return0;}我们本来是想通过f(NULL)调用指针版本的f(int*)函数但是由于NULL被定义成0所以打印了f(int)在C98中字面常量0既可以是一个整形数字也可以是无类型的指针(void*)常量 NULL是(void*) 因为函数参数是 int 所以发生了隐式类型转换如果要将其按照指针方式来使用必须对其进行强转(void *)0。9.3解决因此我们在C11引入了nullptr他就是一个空指针了(void*)0。9.4总结在使用nullptr表示指针空值时不需要包含头文件因为nullptr是C11作为新关键字引入的。在C11中sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。为了提高代码的健壮性在后续表示指针空值时建议最好使用nullptr。