• c++入门


    目录

    前言

    一、c++关键字

    二、命名空间

    2.1命名空间定义

    2.2命名空间的使用 

    三、c++输入&输出

    四、缺省参数

    4.1缺省参数概念

    4.2缺省值的分类

    五、函数重载

    5.1函数重载概念

    5.2c++支持函数重载的原理--名字修饰 

    六、引用

    6.1引用概念

    6.2引用特性 

    6.3常引用 (具有常属性的引用变量)

     6.4使用场景

    七、内联函数

    7.1概念

    7.2特性 

    八、auto关键字(c++11)

    8.1auto简介

     8.2auto的使用细则

    8.3auto不能推导的场景

    九、基于范围的for循环(c++11)

     9.1范围for的语法

     9.2范围for的使用条件

    十、指针空值--nullptr(c++11)



    前言

    c++是在c的基础之上,容纳进去了面向对象的编程思想,并增加了许多有用的库,以及编程范式等。熟悉C语言之后,对c++学习有一定的帮助,本章节主要目标:

            1.补充C语言语法的不足,以及c++是如何对C语言设计不合理的地方进行优化的,比如:作用域方面、IO方面、函数方面、指针方面、宏方面等。

            2.为后续类和对象学习打基础。

    一、c++关键字

     c++总计63个关键字,C语言32个关键字,接下来看看c++有哪些关键字,并不进行具体解释。后期会陆续出现的时候就会讲解。

    asmdoifreturntrycontinue
    autodoubleinlineshorttypedeffor
    booldynamic_castintsignedtypeidpublic
    breakelselongsizeoftypenamethrow

    case

    enummytablestaticunionwchar_t
    catchexplicitnamespacestatic_castunsigneddefault
    charexportnewstructusingfriend
    classexternoperatorswitchvirtualregister
    constfalseprivatetemplatevoidtrue
    const_castfloatprotectedthisvolatilewhile
    deletegotoreinterpret_cast

    二、命名空间

    在C语言中,我们对变量,函数只能定义一次,如果再定义,就会产生命名的冲突,而在c++中,存在一个命名空间可以对标识符的名称进行本地化,以避免命名冲突或名字污染,具体啥意思,这里先举个冲突例子。

    1. #include
    2. #include
    3. int rand = 10;
    4. //rand是头文件中的函数,在这里用来被当做变量,就会产生冲突
    5. //编译就会报错
    6. int main()
    7. {
    8. printf("%d\n", rand);
    9. return 0;
    10. }

    编译结果:

    2.1命名空间定义

    为了解决上述冲突,可以使用命名空间来解决,这里就引入了新的关键字namespace,后面跟命名空间的名字,然后接一对{}即可,{}中即为命名空间的成员

    1.命名空间的正常定义 

    1. #include
    2. #include
    3. namespace test
    4. {
    5. int rand = 10;//在命名空间中,rand就不会与头文件中的rand函数冲突,相当于一堵围墙围了起来
    6. //当然了在同一个命名空间内又不能定义两个一样的变量
    7. int Add(int left, int right)
    8. {
    9. return left + right;
    10. }
    11. struct Node
    12. {
    13. struct Node* next;
    14. int val;
    15. };
    16. }
    17. int main()
    18. {
    19. return 0;
    20. }

    既然存在了命名空间,那么接下来就来学习它的更多用法

    2.命名空间可以嵌套 

    1. #include
    2. #include
    3. namespace N1
    4. {
    5. int a;
    6. int b;
    7. int Add(int left, int right)
    8. {
    9. return left + right;
    10. }
    11. //命名空间可以嵌套,这样又可以定义两个一样的函数,不会冲突,N1与N2中的Add函数不会冲突
    12. namespace N2
    13. {
    14. int c;
    15. int d;
    16. int Add(int left, int right)
    17. {
    18. return left - right;
    19. }
    20. }
    21. }
    22. int main()
    23. {
    24. return 0;
    25. }

    3.同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中。

    1. //在同一个工程中有两个项目test.cpp、test2.cpp,他们有相同命名空间,编译后会合并成一个命名空间
    2. //其实他们就相当于一个命名空间,他们的空间中不能出现定义相同的变量、函数
    3. //test.cpp
    4. #include
    5. #include
    6. namespace T1
    7. {
    8. int a;
    9. int b;
    10. int Add(int left, int right)
    11. {
    12. return left + right;
    13. }
    14. }
    15. int main() {
    16. return 0;
    17. }
    18. //test2.cpp
    19. namespace T1
    20. {
    21. int c;
    22. int d;
    23. int mul(int left, int right)
    24. {
    25. return left * right;
    26. }
    27. }

    对上面稍微总结一下,一个命名空间就定义了一个新的作用域,命名空间中的所有内容都局限于改命名空间中。 

    那么问题来了,外部怎么去访问命名空间里的内容,然道只能命名空间里面自己使用吗?

    且看下面分析。

    2.2命名空间的使用 

    命名空间有三种使用方式  

    • 加命名空间名称及作用域限定符
    1. #include
    2. namespace T1
    3. {
    4. int c = 0;
    5. int d = 0;
    6. int mul(int left, int right)
    7. {
    8. return left * right;
    9. }
    10. struct Node
    11. {
    12. struct Node* next;
    13. int val;
    14. };
    15. }
    16. int main()
    17. {
    18. printf("%d\n", T1::c);//T1为命名空间名称,::叫做作用域限定符,注意是英文形式
    19. //意思是这个作用域限定符限定的域为T1这个域,这样就可以访问域中的成员c
    20. //注意!!!:T1::c会先在全局区域寻找c成员,如果有就会打印在全局区的c,如果没有就会打印在T1域中的c
    21. return 0;
    22. }

    运行结果:

    • 使用using将命名空间中某个成员引入(指定成员展开)

    1. #include
    2. namespace T1
    3. {
    4. int c = 0;
    5. int d = 2;
    6. int mul(int left, int right)
    7. {
    8. return left * right;
    9. }
    10. struct Node
    11. {
    12. struct Node* next;
    13. int val;
    14. };
    15. }
    16. using T1::d;//指定T1域中的d成员展开,将成员d引入到全局区,不再局限于域中,则可以直接访问d成员
    17. //当然也可以继续用作用域限定符访问
    18. int main()
    19. {
    20. printf("%d\n", T1::c);
    21. printf("%d\n", d);//这里就可以直接打印d
    22. return 0;
    23. }

    运行结果:

     

    • 使用using namespace命名空间名称引入 (展开命名空间)
      1. #include
      2. namespace T1
      3. {
      4. int c = 0;
      5. int d = 2;
      6. int mul(int left, int right)
      7. {
      8. return left * right;
      9. }
      10. struct Node
      11. {
      12. struct Node* next;
      13. int val;
      14. };
      15. }
      16. using namespace T1;//直接将命名空间展开,则命名空间里的所有成员都引入到了全局区,可以直接访问
      17. //当然也可以继续用作用域限定符来进行访问
      18. int main()
      19. {
      20. printf("%d\n", T1::c);
      21. printf("%d\n", d);
      22. int ret = mul(10, 20);
      23. printf("%d\n", ret);
      24. return 0;
      25. }

      运行结果:

    注意:std是c++官方库定义的命名空间名,c++将标准库的定义实现都放到这个命名空间中

    ,c++的头文件不在是C语言的,而是,必须包含头文件才能使用std,且看下面使用。

    三、c++输入&输出

    1. #include
    2. using namespace std;//std是c++官方定义的命名空间名,在这里展开std
    3. //我们就可以直接使用std域中的成员(库、函数等)
    4. //如果不展开,就得指明该域
    5. int main()
    6. {
    7. std::cout << "Hello World!" << std::endl;//这里展开std这个域,既可以指明该域,也可以不指明
    8. //接下来进行说明输入/输出
    9. cout << "Hello wordl!" << endl;
    10. int a;
    11. cin>>a;
    12. cout<
    13. return 0;
    14. }

    运行结果:

    1.在C语言中,我们对一些标识符称为变量、函数等,在c++中,要改变一个叫法了, 叫做对象、方法,这里先做了解,在之后篇章会陆续说明

    2.在c++中,引入了新的输入输出对象,分别用cin(标准输入对象)、cout(标准输出对象(键盘)),当然必须包含头文件以及按命名空间使用方法使用std

    3.cout和cin是全局的流对象,endl是特殊的c++符号,就是C语言中的换行"\n"

    4.在这里不是仅仅使用cout和cin进行输出输入,还需要借助<<(流插入运算符),>>(流提取运算符),比如上面内容,意思是将Hello world流入到cout(输出对象控制台),即在控制台输出该字符串,以及从输入对象(键盘)输入数据流入到对象a,再打印a

    5.我们可以发现,使用c++输入输出更方便,不需要像printf/scanf输入输出时那样,需要手动控制格式。c++的输入输出可以自动识别变量类型。

    6.实际上cout和cin对象都是有类型的,分别是ostream和istream类型的对象,跟c++的头文件还挺对应的。>>和<<还涉及到到运算符重载等知识,在第五大点会进行讲解。

    注意说明:早期标准库将所有功能在全局域中实现,声明在.h后缀的头文件中,使用时只需包含对应头文件即可,够来将其实现在std命名空间下,为了和c头文件区分,也为了正确使用命名空间,规定c++头文件不带.h;旧编译器(VC 6.0)中还支持格式,后续编译器已不支持,因此推荐使用+std的方式。

    以下还有几点补充:

    1.cout和cin的注意事项

    1. #include
    2. using namespace std;
    3. int main()
    4. {
    5. int a;
    6. double b;
    7. char c;
    8. //可以自动识别变量的类型
    9. cin >> a;
    10. cin >> b >> c;//可以支持连续输入
    11. cout << a << endl;//可以支持连续输出
    12. cout << b << " " << c << endl;
    13. return 0;
    14. }
    15. //PS:关于cout和cin还有很多更复杂的用法,其实也可以控制浮点数输出精度,控制整形输出进制格式等
    16. //但是c++控制精度有些复杂,而c++兼容C语言的用法,所以可以用C语言来控制精度,而c++控制精度
    17. //用的不多,就不必过多去了解。后续如果有出现,会进行讲解

     运行结果:

    2.std命名空间的使用惯例

    std是c++标准库的命名空间,如何展开std使用更合理呢?

    1.在日常练习中,建议直接using namespace std即可,这样就很方便。

    2.using namespace std展开。标准库就暴露出来了,如果我们定义跟库重名的类型/对象/函数,就存在冲突问题。如果在项目开发中,代码是很多的,就容易出现这样的问题,而我们自己日常练习了就很少出现这样的问题的。所以建议在项目开发中,就带上域限定符,std::cout这样使用指定命名空间和using std::cout展开常用的库对象/类型等方式。

    四、缺省参数

    4.1缺省参数概念

    缺省参数是函数声明或定义时为函数的参数指定一个缺省值。通常来说就是给形参赋值。在调用该函数时,如果没有传实参给形参,那么该函数就会使用该缺省值,否则使用指定的实参

    先看个例子:

    1. #include
    2. using namespace std;
    3. void func(int a = 0)//给缺省值
    4. {
    5. cout << a << endl;
    6. }
    7. int main()
    8. {
    9. func();//不传实参
    10. func(10);//传实参
    11. return 0;
    12. }

    运行结果:

    除此之外,缺省值还有以下分类。

    4.2缺省值的分类

    •  全缺省参数

    全缺省参数就是给所有的形参都给缺省值 

    1. #include
    2. using namespace std;
    3. void func(int a = 0, int b = 10, int c = 20)//给缺省值
    4. {
    5. cout << a << endl;
    6. cout << b << endl;
    7. cout << c << endl;
    8. }
    9. int main()
    10. {
    11. func();//不传实参
    12. func(1,2);//传实参时可以传一个、两个、三个。对应位置没传的用缺省值
    13. //但注意传实参必须是从左边第一个传且连续的,所以func(1, ,3)、func( ,1,2)这些写法是错误的
    14. return 0;
    15. }

    运行结果:

    • 半缺省参数 

     半缺省参数不是说给一半的形参赋值,而是给部分形参缺省值,先给个例子

    1. #include
    2. using namespace std;
    3. void func(int a, int b = 10, int c = 20)//给缺省值
    4. {
    5. cout << a << endl;
    6. cout << b << endl;
    7. cout << c << endl;
    8. }
    9. int main()
    10. {
    11. func(1,2);//传实参时可以传一个、两个、三个。对应位置没传的用缺省值
    12. //但注意传实参必须是从左边第一个传且连续的,所以func(1, ,3)、func( ,1,2)这些写法是错误的
    13. return 0;
    14. }

     运行结果:

    但有几点规则

    1.半缺省参数必须从右往左依次来给出,不能间隔着给

    1. #include
    2. using namespace std;
    3. //若间隔给或者从左往右给,比如:
    4. //间隔给:void func(int a = 10, int b, int c = 10),那么传参时,虽第一个和第三个可给可不给值,但如果不给,而第二个必须给实参,那么就会形成func( , 20, )这种形式
    5. // 而传实参必须是从左边第一个传且连续的,所以间隔传是错误的
    6. //从左往右给:void func(int a = 10, int b = 20, int c),那么传参时,第一个和第二个可给可不给实参,但如果不给,第三个必须给,那么就会形成func( , ,20)这种形式
    7. //而传实参必须是从左边第一个传且连续的,所以从左往右传也是错误的
    8. //从右往左给才是正确的,从右开始给值,如果要继续往左边给值,必须是连续给,当然也可以不给值,比如:
    9. // voif func(int a, int b = 10, int c = 20),那么传参时,第二个第三个可给可不给值,但第一个必须给值,则有func(30)、func(30,40)、func(20,30,40)三种形式
    10. //void func(int a, int b, int c = 10),传参时,第三个可给可不给值,但第一个,第二个必须给值,则有func(10,20)、func(10,20,30)两种形式
    11. void func(int a, int b , int c = 20)//给缺省值
    12. {
    13. cout << a << endl;
    14. cout << b << endl;
    15. cout << c << endl;
    16. }
    17. int main()
    18. {
    19. func(1,2,30);
    20. //但注意传实参必须是从左边第一个传且连续的,所以func(1, ,3)、func( ,1,2)这些写法是错误的
    21. return 0;
    22. }

    运行结果:

    2.缺省参数不能在函数声明和定义中同时出现,只能在声明出现 

    1. //部分代码
    2. //test.h
    3. void func(int a = 10);//在头文件声明函数并给缺省值
    4. //test.cpp
    5. void func(int a = 20)//在源文件中定义也给缺省值
    6. {}
    7. //那么问题来了,调用该函数时,到底是用a=10,还是a=20呢,编译器就无法确定改用哪个缺省值
    8. //所以,缺省值不能在函数声明和定义时同时出现,规定只能在声明中出现,定义时就不给缺省值

    3.缺省值必须是常量或者全局变量

    4.C语言不支持缺省参数 (编译器不支持)

    这两点就不多说了

    五、函数重载

    5.1函数重载概念

     函数重载:是函数的一种特殊情况,c++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数或类型或类型顺序)不同!!!,常用来处理实现功能类似数据类型不同的问题。C语言不支持重载函数。

    误区:注意重载函数与函数返回类型无关,例如,同名函数的返回类型不一样,而函数的参数个数、类型、顺序都一致,这不构成重载函数。

    接下来介绍重载函数的几种形式

    1. #include
    2. using namespace std;
    3. //1.参数类型不同
    4. int Add(int left, int right)
    5. {
    6. cout << "int Add(int left, int right)" << endl;
    7. return left + right;
    8. }
    9. double Add(double left, double right)//参数类型只要有一个与上面int不同就行,也可以全不同,即都是double类型
    10. {
    11. cout << "double Add(double left, double right)" << endl;
    12. return left + right;
    13. }
    14. double Add(int left, double right)
    15. {
    16. cout << "double Add(int left, double right)" << endl;
    17. return left + right;
    18. }
    19. //2.参数个数不同
    20. void f()
    21. {
    22. cout << "f()" << endl;
    23. }
    24. void f(int a)
    25. {
    26. cout << "f(int a)" << endl;
    27. }
    28. //3.参数类型顺序不同,其实也可以看做类型不同,如何分类可以看自己的见解
    29. void func(int a, char b)
    30. {
    31. cout << "func(int a, char b)" << endl;
    32. }
    33. void func(char a, int b)
    34. {
    35. cout << "func(int a, char b)" << endl;
    36. }
    37. int main()
    38. {
    39. Add(10, 20);
    40. Add(9.83, 9.83);//注意:把double Add屏蔽,该调用会执行int Add,因为发生了类型转换,double类型转为了int类型,小友可以去试试
    41. Add(10, 9.83);
    42. f();
    43. f(20);
    44. func(10, 'a');
    45. func('b', 20);
    46. return 0;
    47. }

    运行结果:

    对于重载函数有了一定的了解,但是还是感觉好奇怪,为什么可以支持这样写,原理又是什么,且看下面分析 

    5.2c++支持函数重载的原理--名字修饰 

       在C语言中,我们学习了,一个程序要运行起来,需要经历:预处理、编译、汇编、链接四个阶段

    这里不做解释了,详细可以翻看C语言的内容。 在两个文件中,一个声明了Add函数,一个定义了Add函数,那么链接阶段,会通过符号表找到Add地址,然后将他们链接在一起,那么他们具体是如何寻找符号的呢?

      由于在VS下不好演示这个现象,就在Linux下实验。

    • 采用C语言编译器编译后结果

    通过gcc对C语言编译链接生成Test文件

       

    再通过objdump -S  Test命令查看反汇编代码,在反汇编代码中,发现采用gcc编译完成后,函数名字还是我们写的代码的名称。 

    • 采用c++编译器后结果

    这个cpp文件包含重载函数,那么链接时是怎么区分不会出现同名函数的冲突?

    同理现在利用g++ -c test.cpp编译和g++ -o test.o test.cpp链接,来完成编译的过程,再输入

    objdump -S test.o(这里和C语言查看反汇编代码的指令不一样) 

    六、引用

    6.1引用概念

    引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,他和他引用的变量共用同一块内存空间。

    比如:李逵,在家称为“铁牛”,江湖上人称“黑旋风”

    那么引用变量的形式是什么

     类型& 引用变量名(对象名) = 引用实体;

    1. #include
    2. using namespace std;
    3. void TestRef()
    4. {
    5. int a = 10;
    6. int& ra = a;//定义引用类型,ra仅仅只是a的别名,并没有给ra开空间
    7. //注意:在定义引用时,必须给引用变量名初始化--语法规定(祖师爷的规定)
    8. printf("%p\n", &a);//打印地址也是没有区别的
    9. printf("%p\n", &ra);
    10. }
    11. int main()
    12. {
    13. TestRef();
    14. return 0;
    15. }

     运行结果:

    6.2引用特性 

    1.引用在定义时必须初始化

    2.一个变量可以有多个引用

    3.引用一旦引用一个实体,再不能引用其他实体(就是只能定义一次嘛)

    1. void TestRef()
    2. {
    3. int a = 10;
    4. int& ra = a;//定义引用类型,ra仅仅只是a的别名,并没有实质的开空间
    5. //注意:在定义引用时,必须给引用变量名初始化,语法规定(祖师爷的规定)
    6. //int &ra;//像这样直接定义引用,而不初始化就会报错
    7. int& rra = a;//一个变量可以有多个引用,ra和rra都是a的引用
    8. printf("%p\n", &a);
    9. printf("%p\n", &ra);
    10. printf("%p\n", &rra);
    11. }
    12. int main()
    13. {
    14. TestRef();
    15. return 0;
    16. }

    运行结果:

    6.3常引用 (具有常属性的引用变量)

    1. #include
    2. using namespace std;
    3. void TestRef()
    4. {
    5. const int a = 10;//用const修饰变量a,具有常属性,不可直接修改
    6. //此时再用int &ra = a;会报错
    7. const int& ra = a;//必须加上const修饰才行,为什么,这里有点难理解
    8. //来看const int a = 10;和int &ra = a;这两句。a具有常属性,不可直接修改
    9. //将具有常属性的a赋值给引用变量,中间其实是会产生一个临时变量,临时变量是自带常属性的,临时变量要赋值给引用变量
    10. //而引用变量的类型为int&,引用的实体应该是对应可修改变量的类型,所以将具有常属性的临时变量赋值给可修改类型的引用变量是不可行的。
    11. //这就是一个权限放大的过程,是不行的
    12. //注意:权限可以平移或者缩小,但不能被放大
    13. //那再来看const int a = 10;和 const int& ra = a; a具有常属性,引用变量也具有常属性,可以直接赋值,权限平移
    14. const int& aa = 10;//也是权限的平移,10是一个常量,具有常属性,引用变量也是具有常属性
    15. //那么再来看权限的缩小
    16. int b = 20;
    17. const int& rb = b;//b是可修改类型,而生成的临时变量是具有常属性的,引用变量类型也是具有常属性的,所以临时变量可以赋值给引用变量。
    18. //即由int类型(可修改类型)b转换成具有常属性的不可修改的临时变量,这是权限的缩小。也是OK的
    19. }
    20. int main()
    21. {
    22. TestRef();
    23. return 0;
    24. }

    上面大费周章的说了一下常引用的用法是因为中间产生临时变量 ,那么临时变量是什么,因为什么原因产生的?

    答:顾名思义临时变量是一种临时存在的变量,其实临时变量产生的原因是赋值时,两边变量类型的不一致产生的,且自带常属性,它存放在寄存器,由寄存器管理。

    1. #include
    2. using namespace std;
    3. void TestRef()
    4. {
    5. //像下面这种赋值,就产生了临时变量
    6. int i = 1;
    7. double b = i;//i是int类型,b是double类型,i会生成临时变量,临时变量复制给了b
    8. double d = 12.5;
    9. //int& ra = d;编译会报错,因为引用变量的类型是int&,而d的类型是double,类型不一致
    10. //有人可能会有疑问,他们不会发生隐式转换吗?不会,隐式类型转换的前提是一组相近类型之间的转换
    11. //例如:int double float等等他们都是描述数据的相似类型,之间可发生隐式类型转换。而上述的引用类型与double不一致
    12. const int& rb = d;//像这个,double类型的d会先发生隐式类型转换成int,然后由int类型的d生成临时变量,临时变量具有常属性
    13. //可以赋值给具有常属性的引用变量
    14. }
    15. int main()
    16. {
    17. TestRef();
    18. return 0;
    19. }

     6.4使用场景

    1.做参数

    1. #include
    2. using namespace std;
    3. //在C++中,传参时可以用引用传参,就是给实参取个别名罢了,不会像C语言中的形参一样额外开空间
    4. //好处就是减少了空间的开销
    5. void Swap(int &left, int& right)
    6. {
    7. int temp = left;
    8. left = right;
    9. right = temp;
    10. }
    11. int main()
    12. {
    13. int a = 3, b = 4;
    14. Swap(a, b);//注意:这里如果传的是常量,那么引用时需加上const,理由在临时变量那说的很清楚了
    15. return 0;
    16. }

    2.做返回值 

    先说结论:如果函数返回时,除了函数作用域,如果返回对象还在(还没还给系统),则可以使用引用返回,如果已经还给系统了,则必须使用传值返回。

    来看分析这个代码: 

    1. #include
    2. using namespace std;
    3. int& count()//引用函数可以看做是给这个count函数的别名
    4. {
    5. static int n = 0;//定义了一个静态变量,该对象存放在静态区,不会随着count函数的销毁而销毁
    6. //生命周期是整个main函数的生命周期,只有main函数销毁了才回销毁
    7. n++;
    8. return n;//这里有个细节:对于传值返回的函数在进行返回值时,并不会直接返回这个值,中间其实会产生一个临时变量
    9. //在函数销毁时,这个变量也就是销毁了,返回的实际值其实是这个临时变量的值,对于临时变量存放位置取决于该对象的大小
    10. // 如果比较小的话 4/8bit——>寄存器
    11. //如果比较大的话 —— > 临时变量放在上一个栈帧(调用他的栈帧中)
    12. //
    13. //而对于传引用返回,并不会产生临时变量,返回的是这个值的别名
    14. //
    15. //这里使用了引用返回,且count函数销毁时,n对象并没有销毁,赋值给了引用函数count
    16. }
    17. int main()
    18. {
    19. int& ret = count();//引用函数作为返回值给ret,其实就是相当于返回了n的别名
    20. count();
    21. cout << ret << endl;
    22. return 0;
    23. }

    运行结果:

    再来对比一下这个代码:

    1. #include
    2. using namespace std;
    3. int& Add(int a, int b)
    4. {
    5. int c = a + b;//作用域只限定在这个函数内部,函数销毁时,该变量也会销毁
    6. return c;//此时传引用返回,而c又随着函数的销毁而销毁了,那么此时引用函数的值就是随机值了
    7. //所以对于返回对象出了函数作用域,要还给系统的,必须使用传值返回
    8. }
    9. int main()
    10. {
    11. int& ret = Add(1, 2);
    12. cout << "Add(1,2) is :" << ret << endl;
    13. return 0;
    14. }

    运行结果:

    6.5 传值、传引用效率比较

    对于以值作为传参的参数或者传值返回,并不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,对于传引用返回而言,是直接传回对象的别名,不需要进行拷贝。所以传值返回效率是非常低下的,尤其是对象很大时,还需要拷贝一份,效率更低。

    例如:

    1. #include
    2. using namespace std;
    3. #include
    4. struct A { int a[10000]; };
    5. void TestFunc1(A a){}
    6. void TestFunc2(A& a) {}
    7. void TestValuetime()
    8. {
    9. A a;
    10. //以值作为参数
    11. size_t begin1 = clock();//获取当前时间
    12. for (size_t i = 0; i < 10000; i++)
    13. {
    14. TestFunc1(a);
    15. }
    16. size_t end1 = clock();//循环结束后,再获取当前时间
    17. //以引用作为参数
    18. size_t begin2 = clock();
    19. for (size_t i = 0; i < 10000; i++)
    20. {
    21. TestFunc2(a);
    22. }
    23. size_t end2 = clock();
    24. cout << "TestFunc1-time:" << end1 - begin1 << endl;
    25. cout << "TestFunc2-time:" << end2 - begin2 << endl;
    26. }
    27. int main() {
    28. TestValuetime();
    29. return 0;
    30. }

    运行结果:

    通过代码比较,还是可以发现传值和引用在作为传参以及返回值类型上效率相差很大。

    6.6引用和指针的区别

    我们知道在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。 

    其实在底层实现上实际是有空间的,引用是按照指针方式来实现的。我们写代码,相当于在上层的页面写代码,编译器进行编译时,是对上层的代码进行语法分析、语义分析、符号汇总等等,一定要区分语法概念和底层的区别,所以引用对于上层而言就是一个语法概念的存在

    例子: 

    1. #include
    2. using namespace std;
    3. int main() {
    4. int a = 10;
    5. int& ra = a;
    6. ra = 20;
    7. int* pa = &a;
    8. *pa = 20;
    9. return 0;
    10. }

     该代码反汇编(先进入调试模式、右击代码,选中转反汇编):

     虽然他们在底层上实现是一样的,但在上层引用和指针在各方面还是有区别的:

    1.引用概念上定义一个变量的别名,指针存储一个变量地址

    2.引用在定义时必须初始化,指针没有要求

    3.引用在初始化时引用一个实体后,就不能引用其他实体,而指针可以在任何时候指向任何一个同类型实体

    4.没有NULL引用,但有NULL指针

    5.在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台占4个字节,64位平台占8个字节)

    6.引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小

    7.有多级指针,但是没有多级引用(不管被引用多少次,始终是一个变量的别名)

    8.访问实体方式不同,指针需要显示解引用,引用编译器自己处理

    9.引用比指针使用起来相对更安全(引用定义时就与变量绑定,也不用操作地址,而指针定义时未初始化就是野指针,还可以通过修改地址来访问变量)

    七、内联函数

    7.1概念

    以inline修饰的函数叫做内联函数,编译时c++编译器会在调用函数的地方展开,而不会建立栈帧,提升了程序运行的效率

    例子: 

    1. #include
    2. using namespace std;
    3. int Add(int left, int right)
    4. {
    5. return left - right;
    6. }
    7. int main()
    8. {
    9. int ret = 0;
    10. ret = Add(1, 2);
    11. return 0;
    12. }

     查看反汇编代码,发现一个call指令

     如果在上述函数前增加inline关键字将其改成内联函数,在编译器期间编译器会将函数体替换函数的调用

    两个注意点:

    1.在debug环境下,默认不会对编译器优化,所以要查看inline函数得先开启设置,不然的话,即使加了inline,还是看不到inline函数展开的过程

    2.release环境下,不管什么,对编译器进行了很强大的优化

    debug环境: 

    右击该解决方案,选择属性,选择以下内容

    设置好后,再查看反汇编,此时没有了call指令 

     release环境:

    在release版本下,发现优化的更加厉害,前面的啥指令都不调用了, 也不开空间了。

    7.2特性 

    1.inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,则在编译阶段函数体会替换函数调用,优点:少了调用开销,提高程序运行效率,缺点:可能使目标文件变大

    2.inline只是给编译器提供一个展开建议,至于实不实现展开看编译器,一般来说:函数规模较小、不是递归、且频繁调用的inline函数编译器会展开函数体,否则会忽略其特性。 

    3.inline不建议声明和定义分离,分离会导致链接错误。因为inline函数被展开,就没有了函数地址,就没有了call指令,那么在链接时就会找不到定义 。look

    1. //fun.h
    2. #include
    3. using namespace std;
    4. inline void fun(int i);
    5. //fun.cpp
    6. #include "fun.h"
    7. void f(int i)
    8. {
    9. cout << i << endl;
    10. }
    11. //test.cpp
    12. #include "fun.h"
    13. int Add(int left, int right)
    14. {
    15. return left - right;
    16. }
    17. int main()
    18. {
    19. int ret = 0;
    20. ret = Add(1, 2);
    21. return 0;
    22. }

     编译结果:

    八、auto关键字(c++11)

    8.1auto简介

    在早起 c/c++中auto的含义:使用auto修饰的变量,是具有自动存储器的局部变量,c++11中,标准委员会赋予了auto全新的含义:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。

    意思就是用auto声明的变量类型将会由赋值的类型决定,在编译时,auto则会指示编译器检测赋值的类型来推导变量的类型,且auto会替换成变量实际的类型。所以auto并不是一种类型的声明,而是一个类型声明时的“占位符”,且在定义时必须要初始化!!!

    例子: 

    1. #include
    2. using namespace std;
    3. int TestAuto()
    4. {
    5. return 10;
    6. }
    7. int main() {
    8. int a = 10;
    9. auto b = a;
    10. auto c = 'a';
    11. auto d = TestAuto();
    12. //使用typeid(变量名称).name()可以识别变量的类型
    13. cout << typeid(a).name() << endl;
    14. cout << typeid(b).name() << endl;
    15. cout << typeid(c).name() << endl;
    16. cout << typeid(d).name() << endl;
    17. return 0;
    18. }

    运行结果:

     

     8.2auto的使用细则

     1.auto与指针和引用结合起来使用

    用auto声明指针类型时,用auto和auto*没有任何区别,单用auto声明引用类型是则必须加&

    例子: 

    1. #include
    2. using namespace std;
    3. int main() {
    4. int a = 10;
    5. auto b = &a;
    6. auto* c = &a;
    7. auto& d = a;
    8. //使用typeid(变量名称).name()可以识别变量的类型
    9. cout << typeid(b).name() << endl;
    10. cout << typeid(c).name() << endl;
    11. cout << typeid(d).name() << endl;
    12. *b = 20;
    13. *c = 30;
    14. d = 40;
    15. return 0;
    16. }

     运行结果:

    2.在同一行定义多个变量

    当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译期将会报错,因为编译器实际只对一个类型进行推导,然后用推导出来的类型定义其他变量 

    例子:

    1. #include
    2. using namespace std;
    3. int main() {
    4. auto a = 1, b = 2;
    5. auto c = 3, d = 4.0;//编译报错,初始化类型不一致
    6. return 0;
    7. }

     编译结果:

    8.3auto不能推导的场景

     1.auto不能作为函数的参数

    1. #include
    2. using namespace std;
    3. //编译报错,编译器无法对a的实际类型进行推导
    4. void TestAuto(auto a)
    5. {}
    6. int main() {
    7. TestAuto(2);
    8. return 0;
    9. }

    编译结果:

    2.auto不能直接用来声明数组

    1. #include
    2. using namespace std;
    3. int TestAuto()
    4. {
    5. int a[] = { 1,2,3 };
    6. auto b[] = { 4, 5, 6 };//编译报错,auto只能推导变量的类型
    7. //而数组不仅有元素类型、还有数组大小,数组的大小必须在编译时是已知的,
    8. //而auto在编译时不能推导数组的大小
    9. }
    10. int main() {
    11. TestAuto();
    12. return 0;
    13. }

     编译结果:

    3.为了避免与c++98中的auto发生混淆,c++11只保留了auto作为类型指示符的用法

    4.auto在实际中最常见的优势用法就是跟后面的c++提供的新式for循环,还有lambda表达式等进行配合使用 。

    九、基于范围的for循环(c++11)

     9.1范围for的语法

    对于一个有范围的集合,又要去说明循化的范围是多余的,有时候还会犯错误。因此c++11中引入了基于范围的for循环。for循环后的括号由冒号“   :”分为两部分,第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围

    1. #include
    2. using namespace std;
    3. void TestAuto()
    4. {
    5. int array[] = { 1,2,3,4,5 };
    6. for (auto& e : array)//遍历array数组,自动识别数组的元素、大小,每次遍历的结果都会放到引用变量e中
    7. {
    8. cout << e << " ";
    9. }
    10. cout << endl;
    11. for (int& e : array)//知道array的元素类型,也可以直接指明引用类型
    12. {
    13. cout << e << " ";
    14. }
    15. cout << endl;
    16. for (int e : array)//当然也可以直接放到一个变量中
    17. {
    18. cout << e << " ";
    19. }
    20. }
    21. int main() {
    22. TestAuto();
    23. return 0;
    24. }

     运行结果:

     9.2范围for的使用条件

    1.for循环迭代的范围必须是确定的

    对于数组而言,即第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的用法,begin和end就是for循环迭代的范围

    例子:

    1. #include
    2. using namespace std;
    3. //数组作为函数参数时会自动转化为指向第一个元素的指针。因此,在这里,形参array实际上是一个指向int类型的指针,而不是一个数组。
    4. //因此for的循环范围不确定
    5. void TestAuto(int array[])
    6. {
    7. for (auto& e : array)
    8. cout << e << endl;
    9. }
    10. int main() {
    11. int array[] = {1,2,3};
    12. TestAuto(array);
    13. return 0;
    14. }

    编译结果:

    2.迭代的对象要实现++和==的操作。(关于迭代器这个问题,在之后小编的文章中会给出,这里提一下) 

    十、指针空值--nullptr(c++11)

    在C语言中,我们知道NULL实际其实是一个宏,放在传统的c头文件(stddef.h)中。在C语言中和在c++中NULL的宏定义还是有区别的

    来看他们的定义: 

     转到NULL的定义中去,可以发现在c++中NULL就是0,而在C语言中为((void*)0),正是因为有这两种存在,在使用空值的指针时,就有可能会遇到冲突。look

    1. #include
    2. using namespace std;
    3. void f(int)
    4. {
    5. cout << "f(int)" << endl;
    6. }
    7. void f(int*)
    8. {
    9. cout << "f(int*)" << endl;
    10. }
    11. int main() {
    12. f(0);
    13. f(NULL);//在c++中这里就被当成了0,都会执行f(int),输出结果一致
    14. return 0;
    15. }

    运行结果:

    其实早在c++98中,字面常量0既可以是一个整型数字,也可以是无类型的指针(void*)常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void*)0 。

    那么在c++11中为了避免这种方式,引入了新的关键字nullptr来表示空指针,不需要包含头文件,nullptr跟(void*)0是等价的,且为了提高代码的实用性,建议后续最好使用nullptr表示空指针。

    end~ 

  • 相关阅读:
    【产品经理修炼之道】- 从需求到功能的转化过程
    box-shadow用法详解
    进程间通信学习笔记(有名管道和无名管道)
    麒麟信安组织开展国产操作系统技术赋能专题培训
    细节决定成败!jdbc的List<?> qryList4Sql(String sql)报错-标志符过长
    【云原生 | Kubernetes 系列】----亲和与反亲和
    排序算法的稳定性
    WEEX编译|加密市场三季度回顾及未来展望
    VUE3 之 动态组件 - 这个系列的教程通俗易懂,适合新手
    PCB(一):altium designer 环境安装配置
  • 原文地址:https://blog.csdn.net/weixin_68201503/article/details/134300388