当前位置:   article > 正文

C++入门(一)之基础知识点_c++ 基础知识

c++ 基础知识

目录

零、什么是c++

一、c++关键字

二、c++的命名空间

命名冲突

命名空间的定义

命名空间的使用

三、c++中的输入和输出

四、缺省参数

缺省参数的概念:

半缺省函数注意事项:

缺省函数的作用

五、函数重载(重要)

extern “C”

六、引用

引用实例

引用特性

常引用

实例1 

实例2 

实例3 

实例4

引用使用场景

1.0 做参数

2.0 做返回值

(1)值返回

(2)引用返回

值和引用作为返回值的性能比较

引用的作用主要体现在传参和传返回值

引用与指针的区别

引用和指针的不同点:

六、内联函数

内联函数特性

七、auto关键字(C++11)

auto的使用细则

auto不能推到的场景

八、 基于范围的for循环(C++11)

范围for使用条件

 九、指针空值nullptr(C++11)


零、什么是c++

     C语言是结构化和模块化的语言,适合处理较小规模的程序。对于复杂的问题,规模较大的程序,需要高度的抽象和建模时,C语言则不合适。为了解决软件危机,
 20世纪80年代, 计算机界提出了OOP(objectoriented programming:面向对象)思想,支持面向对象的程序设计语言应运而生。
     1982年,BjarneStroustrup博士在C语言的基础上引入并扩充了面向对象的概念,发明了一种新的程序语言。为了表达该语言与C语言的渊源关系,命名为C++。因此:C++是基于C语言而产生的,它既可以进行C语言的过程化程序设计,又可以进行以抽象数据类型为特点的基于对象的程序设计,还可以进行面向对象的程序设计。

此刻,以熟悉的hello world 开启c++的学习

  1. #include<iostream>
  2. using namespace std;
  3. int  main()
  4. {
  5.     cout << "hello world" << endl;
  6.     return 0;
  7. }

一、c++关键字

在这里我们可以先了解一下

 二、c++的命名空间

命名冲突

或许很多人都知道在编写一个cpp程序时要加入 using  namespace  std ;但你知道这究竟是为什么嘛?
———在C/C++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。

namespace定义的是一个域,本质是解决C语言命名冲突的问题

eg:

针对以上程序我们的期望是,19行的scanf调用的是函数scanf,其他的scanf调用的是我们定义的变量,但是程序调用时遵循的是就近原则(从局部域开始找),19行的scanf调用不会去<stduio.h>中调用函数,而是调用了离它最近的scanf变量(16行),所以就产生了命名冲突的问题,C语言是解决不了这个问题的。

如果将他俩放到全局,就会出现重定义的问题 

eg:

通过namespace就可以完美解决问题

命名空间的定义

1. 定义命名空间首先用关键字namespace,在关键字后面加上名称,再加一对{}即可

  1. namespace n1  //n1 为命名空间的名称
  2. {
  3.     int a = 0;  //命名空间中可以定义函数,也可定以变量
  4.     int add(int a, int b)
  5.     {
  6.         return a + b;
  7.     }
  8. }

2. 命名空间可以嵌套

  1. namespace n2
  2. {
  3.     namespace n3
  4.     {
  5.         namespace n4
  6. {
  7. //...
  8. }
  9.     }
  10. }

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

  1. namespace n1
  2. {
  3.     int Mul(int left, int right)
  4.     {
  5.         return left * right;
  6.     }
  7. }

注意:一个命名空间定义了一个新的作用域,命名空间中的所有内容都局限于该命名空间当中

命名空间的使用

C++库为了防止命名冲突,把自己库里面的的东西都定义在一个std的命名空间中去,要使用标准库中的东西,有三种方式

ps:#include<iostream.h> 老一点的C++标准用这个,VC6.0 可以用,没有std命名空间

1.0 指定命名空间 ,麻烦,每个地方要用都要指定,但它是最规范的方式

2.0 吧std整个展开,相当于库里面的东西都到全局域里面去了,看起来方便了,如果我们自己定义的东西跟库起冲突了,就没法解决。一夜回到解放前,所以规范的工程项目中是不推荐这种方式的,日常练习无所谓

3.0 对部分常用的库里面的东西展开

PS: ::  这是域作用限定符,C语言调用全局变量可以用

定义了如下的空间N1 ,使用它一共有三种使用方式

  1. #include <iostream>
  2. namespace N
  3. {
  4.     int a = 10;
  5.     int b = 20;
  6.     int add(int left, int right)
  7.     {
  8.         return left + right;
  9.     }
  10.     int sub(int left, int right)
  11.     {
  12.         return left - right;
  13.     }
  14. }
  15. int main()
  16. {
  17.     //printf("%d", a);//直接使用编译器会报错
  18. }

1.0 加命名空间名称及作用域限定符   什么时候用什么时候打开空间,安全,但是较为复杂

  1. int main()
  2. {
  3.     printf("%d ", N::a);
  4.     printf("%d ", N::b);
  5. }

2.0  使用using namespace 命名空间名称引入,不安全但是简单

  1. using namespace N;
  2. int main()
  3. {
  4.     printf("%d ", a);
  5.     printf("%d ", b);
  6.     int c=add(1, 2);
  7.     printf("%d ", c);
  8.     int d=sub(4, 3);
  9.     printf("%d ", d);
  10. }

3.0 使用using将命名空间中成员引入  ,用哪个,提前打开哪个对象安全,推荐使用

  1. using N::b;
  2. int main()
  3. {
  4.     printf("%d ", N::a);
  5.     printf("%d ", b);
  6. }

想必到达这里就会明白为什么要用using namespace std;这一串话了(这段话告诉编译器我们要用名字空间std中的函数或者对象)
但是日后当我们在写成熟的代码的时候,一般不建议将标准命名空间全部打开,而是需要用库里的什么就打开什么。这就有效的防止了命名冲突

例如写一个hello world
第一种就是上面提到的
使用using namespace 命名空间名称引入,不安全但是简单

  1. #include <iostream>
  2. using namespace std;
  3. int main()
  4. {
  5.     cout << "hello,world" << endl;
  6.     return 0;
  7. }

第二种,用哪个,提前打开那个对象
安全,推荐使用

  1. #include <iostream>
  2. using std::cout;
  3. using std::endl;
  4. int mian()
  5. {
  6.     cout << "hello world" << endl;
  7. }

第三种,什么时候用什么时候打开std
安全,但是较为复杂

  1. #include <iostream>
  2. int main()
  3. {
  4.     std::cout << "hello world" << std::endl;
  5. }

三、c++中的输入和输出

ostream 类型全局对象 cout

istream 类型全局对象 cin

endl 全局换行符号

又是老朋友:hello world

  1. #include <iostream>
  2. using namespace std;
  3. int main()
  4. {
  5.     cout << "hello,world" << endl;
  6.     return 0;
  7. }

对比C语言的scanf 与 printf ,C++的cout与cin的区别是什么

cout与cin可以自动识别类型

eg: 

但cin,cout也不是在任何情况下都好用,

eg:

  1. #include<iostream>
  2. using namespace std;
  3. struct Person
  4. {
  5. char name[10];
  6. int age;
  7. };
  8. int main()
  9. {
  10. int a = 0;
  11. cin >> a; //scanf("%d ",&a);
  12. cout << a << endl;
  13. //这里cin就比scanf好很多
  14. struct Person p = { "小王",10 };
  15. printf("name:%s age:%d\n", p.name, p.age);//格式化输出printf更好一点
  16. cout << "name: " << p.name << "\n" << "age:" << p.age << endl;
  17. return 0;
  18. }

 综合而言,两者可以混着用,谁好用谁。
2. 使用C++输入输出更方便,不需增加数据格式控制,比如:整形--%d,字符--%c

  1. #include <iostream>
  2. using namespace std;
  3. int main()
  4. {
  5.     int a;
  6.     double b;
  7.     char c;
  8.     cin >> a; // >> 流提取操作符
  9.     cin >> b >> c;
  10.     cout << a << "  " << b << " " << c << endl; // << 流插入操作符
  11.     return 0;
  12. }

四、缺省参数

缺省参数的概念:

     缺省参数是声明或定义函数时为函数的参数指定一个默认值。在调用该函数时,如果没有指定实参则采用该默认值,否则使用指定的实参。缺省参数又分为全缺省与半缺省。

说白了缺省函数就是,C++函数的参数中配的备胎

1.0全缺省,全部的参数都给了一个缺省值

  1. #include<iostream>
  2. using namespace std;
  3. void fun(int a = 10, int b = 20, int c = 30)
  4. {
  5. cout << "a= " << a << endl;
  6. cout << "b= " << b << endl;
  7. cout << "c= " << c << endl << endl;
  8. }
  9. int main()
  10. {
  11. fun(); //不传参就用默认的
  12. fun(1, 2, 3); //传了就用传了的
  13. fun(1); //传了部分
  14. fun(1, 2); //传了部分
  15. }

执行结果:


 

 2.0 半缺省参数,部分的参数给了缺省值

  1. #include<iostream>
  2. using namespace std;
  3. void fun2(int a, int b, int c = 30)
  4. {
  5. cout << "a= " << a << endl;
  6. cout << "b= " << b << endl;
  7. cout << "c= " << c << endl << endl;
  8. }
  9. int main()
  10. {
  11. fun2(1, 2); // 没传就用默认值
  12. fun2(1, 2, 3); // 传了就用传了的
  13. }

执行结果: 

半缺省函数注意事项:

 1. 半缺省参数必须从右往左依次来给出,不能间隔着给
    正确的半缺省函数写法:

错误的半缺省函数写法:

原因也很简单,因为函数传参的时候是从左向右依次传参的

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

注意:如果生命与定义位置同时出现,恰巧两个位置提供的值不同,那编译器就无法确定到底该用那个缺省值。

  1. //a.h
  2. void TestFunc(int a = 10);
  3. // a.c
  4. void TestFunc(int a = 20)
  5. {}

   3.缺省值必须是常量或者全局变量
   4.C语言不支持(编译器不支持)

缺省函数的作用

   缺省函数的作用就是为了使函数调用更加灵活,乍一听感觉好像也没啥用,但在一些情况下发挥着巨大的作用;

eg:我在这写了个栈及栈的初始化,初始化我写的是开四个空间,在主函数中,假设我知道栈里面至少要存100个数据,这个初始化显然不合理只能开4个,接着就开始增容,增到100,同时增容是要付出代价的,有消耗的,所以就很不方便。但假设我知道这个栈里面最多存10个数据,而对这个栈来说,我们写的初始化就很合理,增容的消耗也小。所以就造成了不方便的局面。

  1. struct Stack
  2. {
  3. int* a;
  4. int size;
  5. int capacity;
  6. };
  7. void StackInit(struct Stack* ps)
  8. {
  9. ps->a = (int*)malloc(sizeof(int) * 4);
  10. ps->size = 0;
  11. ps->capacity = 0;
  12. }
  13. int main()
  14. {
  15. struct Stack st1;
  16. //假设我知道栈里面至少要存100个数据
  17. StackInit(&st1);
  18. //这个初始化显然不合理,接着就开始增容,增到100,同时增容是要付出代价的,有消耗的
  19. struct Stack st2;
  20. //假设我知道这个栈里面最多存10个数据
  21. StackInit(&st2);
  22. //而对这个栈来说,我们写的初始化就很合理,增容的消耗也小
  23. return 0;
  24. }

这时候缺省参数的作用就体现出来了,我们将初始化的函数多传一个缺省值4

  1. void StackInit(struct Stack* ps, int  InitCapacity = 4)
  2. {
  3.     ps->a = (int*)malloc(sizeof(int) * InitCapacity);
  4.     ps->size = 0;
  5.     ps->capacity = InitCapacity;
  6. }

假设我知道栈里面至少要存100个数据,缺省值就传100。

假设我知道这个栈里面最多存10个数据缺省值就传10。

假设我不知道这个栈里面可能存多少数据,缺省值就啥也不传,非常方便的解决了问题,同时体现出缺省函数的作用:使函数调用更加灵活。

  1. int main()
  2. {
  3. struct Stack st1;
  4. //假设我知道栈里面至少要存100个数据
  5. StackInit(&st1,100);
  6. struct Stack st2;
  7. //假设我知道这个栈里面最多存10个数据
  8. StackInit(&st2,10);
  9. struct Stack st3;
  10. //假设我不知道这个栈里面可能存多少数据
  11. StackInit(&st2);
  12. return 0;
  13. }

五、函数重载(重要)

     自然语言中,一个词可以有多重含义,人们可以通过上下文来判断该词真实的含义,即该词被重载了。( 函数重载必须在同一作用域 )

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

     C语言不允许定义同名函数,C++可以,要求就是函数的(参数个数 或 类型 或 顺序)必须不同。

 eg:下列函数都够成重载

  1. #include<iostream>
  2. using namespace std;
  3. int Add(int left, int right)
  4. {
  5. return left + right;
  6. }
  7. char Add(char left, char right)
  8. {
  9. return left + right;
  10. }
  11. double Add(double left, double right)
  12. {
  13. return left + right;
  14. }
  15. long Add(long left, long right)
  16. {
  17. return left + right;
  18. }
  19. int main()
  20. {
  21. cout << Add(10, 20) << endl; //匹配int型
  22. cout << Add(10.0, 20.0) << endl; //匹配double型
  23. cout << Add(10L, 20L) << endl; //匹配long型
  24. cout << Add('1', '2') << endl; //匹配char型
  25. return 0;
  26. }

结果如下:

这两个函数构成重载吗? 

构成重载,但是不能同时调用,因为编译器识别不了到底应该调用谁 

注意:这两个函数就不构成运算符重载,因为参数的类型,个数都一样,只是返回值不同

  1. short Add(short left, short right) 
  2. {
  3.  return left+right;
  4. }
  5. int Add(short left, short right)
  6.  {
  7.  return left+right;
  8.  }

下面两个函数构成重载吗?

  1. void TestFunc(int a = 10) {
  2.  cout<<"void TestFunc(int)"<<endl; }
  3. void TestFunc(int a) {
  4.  cout<<"void TestFunc(int)"<<endl; }

答案:不构成,他俩类型相同,只是缺省参数不同`

1.0为什么C语言不支持重载,C++支持重载?C++底层是如何支持重载的

以这单代码为例

编译链接的过程

第一步 预处理---头文件展开+宏替换+去掉注释+条件编译--func.i test.i

第二步 编译--检查语法,生成汇编代码--func.s test.s

第三步 汇编--把汇编代码转成二进制机器代码--func.o test.o

第四步 链接--把.o文件链接到一起,合并符号表,生成可执行程序--a.out

C语言不支持函数重载,因为编译的时候,两个函数重载,函数名相同,在func.o符号表中存在歧义和冲突,其次在链接的时候也存在歧义和冲突,因为他们都是直接通过使用函数名去标识和查找。而重载函数,函数名相同。

C++的目标文件符号表中不是直接用函数名来标识和查找函数。它引入了函数名修饰规则(不同编译器下不同)。

g++的函数名修饰规则_Z+函数名长度+函数名+参数首字母

有了函数名修饰规则,只要参数不同,func.o符号表里面重载的函数就不存在二义性和冲突了。链接的时候,test.o的mian的函数里面去调用两个重载的函数,查找地址时也是明确的。

ps:main函数中调用函数时,如果当前文件有函数的定义,那么编译的时候就填上地址了;如果当前文件只有函数的声明,那么定义就在其他.cpp中,编译时没有地址,只能在链接的时候去其他.o符号表中根据函数修饰名字去找,这就是链接的重要工作。 

extern “C”

实际场景中可能出现两种情况

1.有个东西是C++写的,现在有个C语言的程序,需要调用这个东西

2.有个东西是C写的,现在有个C++的程序,需要调用这个东西

六、引用

引用的概念:引用不是定义一个新的变量,**而是给已知的变量取一个别名**,编译器不会为引用变量开辟内存空间,它和它引用的的变量共用同一块内存空间。 (通俗来说,引用就是取别名)
比如:李逵,在家称为"铁牛",江湖上人称"黑旋风”。

  1. int main()
  2. {
  3. int a = 10;
  4. cout << "a:" << a << endl;
  5. //&在类型后面,就是引用的意思
  6. int& b = a; // b就是a的引用(别名)
  7. b = 20;
  8. cout << "a:" << a << endl;
  9. int& c = b; //c就是b的引用
  10. }

通过监视我们可以看出完全符合引用的概念。

引用实例

1.0 我们最熟悉的也就是交换两个值

  1. void swap(int* a, int* b) //C语言中的交换需要指针(形参的改变不影响实参的改变)
  2. {
  3. int tmp = *a;
  4. *a = *b;
  5. *b = tmp;
  6. }
  7. void swap(int& a, int& b) //C++的交换,用引用即可
  8. {
  9. int r = a;
  10. a = b;
  11. b = r;
  12. }
  13. int main()
  14. {
  15. int a = 10;
  16. int b = 20;
  17. swap(&a, &b); //C语言传地址
  18. swap(a, b); //C++传引用
  19. }

引用特性

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

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

  1. int main()
  2. {
  3.     //一个变量可以有多个别名
  4.     int a = 0;
  5.     int& b = a;
  6.     int& c = b;
  7.     int& d = c;
  8. }

3. 引用一旦引用一个实体,再不能引用其他实体

对于指针:

 由监视可以看出,p的指向发生了改变

 对于引用

 由监视可知,开始引用了a,就只能引用a,不能去引用x。

常引用

首先明白const关键字 ,const修饰默一遍量就代表该变量不能修改了

实例1 

const修饰代表a不能修改
 int& ra = a;   错误原因: ra引用a属于权限的放大,a是可读的,ra却是可读可写的,所以不行
 const int& ra = a;正确原因:  加上const后ra也变成只能读了,和原先的a权限保持一致,所以正确

实例2 

const int& crb = b;//正确原因,crb引用b属于权限的缩小,b是可读可写的,crb是可读的,权限缩小,所以可行

实例3 

首先明白

 上述转换是可以的,发生了隐式类型转换,首先产生一个中间临时变量,这个中间临时变量是double类型的,将c赋给临时变量,再将临时变量赋给d

   double& rc = c; 很多人说这种写法错误的原因是:rc是double类型,c是int类型的,这种解释是错误的。 而我们发现加了个const后就可以编过了,原因:发生类型赋值过程当中,将int赋给double,中间就会产生临时变量,临时变量是double类型,所以rc引用的是这个临时变量,而这个临时变量具有常性(只是可读的),所以加了const后编译通过,类似于eg1。

实例4

对这种传值,传a与c都是没有问题的

如果 假设x是一个大对象或者说是深拷贝的对象,那么尽量用引用传参,减少拷贝,但这时候a就属于权限的放大,所以程序编译不过

 

 加入const就可以解决,如果f函数中不改变x,建议尽量用const引用传参

引用使用场景

1.0 做参数

C语言中交换需要传地址,因为形参是实参的拷贝

  1. void Swap(int* left, int* right)
  2. {
  3. int temp = *left;
  4. *left = *right;
  5. *right = temp;
  6. }

C++中直接传引用就可以实现交换,既没有传值也没有传地址,传的是引用 

  1. void Swap(int& left, int& right)
  2. {
  3.  int temp = left;
  4.   left = right;
  5.   right =temp;
  6.  }

同样,他俩也构成重载,但是调用swap的时候会存在歧义,编译器不知道该调用谁

传值、传引用效率比较
  1. #include <time.h>
  2. struct A { int a[10000]; };
  3. void TestFunc1(A a) {}
  4. void TestFunc2(A& a) {}
  5. void TestRefAndValue()
  6. {
  7. A a;
  8. // 以值作为函数参数
  9. size_t begin1 = clock();
  10. for (size_t i = 0; i < 10000; ++i)
  11. TestFunc1(a);
  12. size_t end1 = clock();
  13. // 以引用作为函数参数
  14. size_t begin2 = clock();
  15. for (size_t i = 0; i < 10000; ++i)
  16. TestFunc2(a);
  17. size_t end2 = clock();
  18. // 分别计算两个函数运行结束后的时间
  19. cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
  20. cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
  21. }
  22. int main()
  23. {
  24. TestRefAndValue();
  25. }

ps:使用引用传参,如果函数中不改变参数的值,建议使用const Type & ,因为它可以接受各种类型的对象

eg:

  1. void StackPrint(const struct Stack& st)
  2. {}

2.0 做返回值

(1)值返回


     传值返回时(所有的传值返回都会生成一个拷贝),调用Add完成后会产生int类型的临时变量,c给了临时变量,临时变量再给给ret
返回的对象是c的拷贝

  通过下图我们可以证明确实产生了临时变量(ret引用的不是c,其实是这个临时变量,临时变量具有常性,所以第二个引用编过了)

为什么会产生临时变量呢?

如果直接把c给给ret会出现很多问题,函数Add执行完成以后,函数栈帧就销毁了,再去取c的值给给ret就会出现很多问题,有可能返回3,也有可能返回随机值,这取决于栈帧销毁的时候清不清理空间,严格意义上来说都是非法访问的,一块空间已经还给系统(销毁),我们就不能去访问了 ,所以说中间就会产生一个临时变量

这个临时变量存在哪呢? 

1.0 如果c比较小(4个字节或者8个字节),一般是寄存器充当临时变量。

2.0如果c比较大,临时变量放在调用Add函数的栈帧中去

(2)引用返回

 传引用返回 ,意思就是不会生成c的拷贝返回,直接返回c的引用

 主函数中并没有加const编译通过,所以证明返回的是c的引用

这段代码看着是没问题,编译器也通过了,但它实际上却存在着问题:

1.0 存在非法访问,因为Add(1,2)的返回值是c的引用,所以Add栈帧销毁以后,回去访问c位置的空间

2.0 如果Add函数栈帧销毁时,清理空间,那么取c值的时候取到的就是随机值,给ret就是随机值 ,这个取决于编译器的实现。ps:vs下销毁栈帧是没有清理空间数据的

 编译器有时候也会清理空间数据,例如对malloc函数

 所以现在的用法是一个错误的用法。

那么如果所有的编译器在栈帧销毁时都不清理空间,那是不是就可以这样用了?

答案也是不行的。

 这个时候ret就是c的引用(别名),就是c。为什么ret是3,最终会变成30呢?

第一次调用Add函数建立栈帧,调用完成后销毁,此时ret也就是c的值是3,然后再调用一次Add函数两次函数调用的栈帧大小是一样的,覆盖了同样位置的空间,c变成是30,函数栈帧销毁,再次打印ret,ret就是c,变成了30

PS:不同的栈帧不一定在同一块空间,原来的空间已经销毁了,在调用函数的时候就可以继续用之前的                                    

cout同样也会建立栈帧,每个栈帧的大小取决于这个函数里面会开辟多少,这个函数里面的局部变量和参数越多,栈帧就越大,cout同样也是一个函数,他为什么没有影响ret,就说明cout的栈帧开的很小,没有覆盖到c,如果覆盖到c,那么c就会改变

eg:

这就说明printf开的栈帧将c给覆盖掉了 

栈帧是调用完了就销毁掉,销毁掉以后,别人调用这可以利用这块栈帧,没对c覆盖就没影响,对c覆盖了就有影响

注意:一个函数调用就会向下空间建立一个栈帧,函数调用结束,栈帧就会销毁

引用返回的使用情况
实际过程中,出了函数作用域,返回对象就不存在了,不能用引用返回,但是加了static保证他出了作用域不会被销毁,就可以用引用返回了



总结下:如果函数返回时,出了函数作用域,如果返回对象还未还给系统(也就是没销毁,例如加了static),则可以使用引用返回,如果已经还给系统了,则必须使用传值返回。

值和引用作为返回值的性能比较

以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是 传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是 当参数或者返回值类型非常大时,效率就更低。
  1. #include <time.h>
  2. struct A { int a[10000]; };
  3. A a;
  4. // 值返回 ---每次拷贝40000字节
  5. A TestFunc1() { return a; }
  6. // 引用返回 ---没有拷贝
  7. A& TestFunc2() { return a; }
  8. void TestReturnByRefOrValue()
  9. {
  10. // 以值作为函数的返回值类型
  11. size_t begin1 = clock();
  12. for (size_t i = 0; i < 100000; ++i)
  13. TestFunc1();
  14. size_t end1 = clock();
  15. // 以引用作为函数的返回值类型
  16. size_t begin2 = clock();
  17. for (size_t i = 0; i < 100000; ++i)
  18. TestFunc2();
  19. size_t end2 = clock();
  20. // 计算两个函数运算完成之后的时间
  21. cout << "TestFunc1 time:" << end1 - begin1 << endl;
  22. cout << "TestFunc2 time:" << end2 - begin2 << endl;
  23. }
  24. int main()
  25. {
  26. TestReturnByRefOrValue();
  27. }

调用一万次比较他们的性能,结果如下

由此发现,引用返回比值返回快很多 

引用的作用主要体现在传参和传返回值

1.0 引用传参和传返回值,在有些场景下,可以提高性能(当参数和返回值是比较大的变量时,传引用传参和传引用做返回值可以提高效率,深拷贝...)
2.0 引用传参和传返回值,输出型参数和输出型返回值。通俗来说,在有些场景下,形参的改变可以改变实参,有些场景下,引用返回,可以改变返回对象。

引用与指针的区别

  1. int main()
  2. {
  3.     int a = 10;
  4.     //在语法上,这里给a这块空间取了一块别名,没有开辟新空间
  5.     int& ra = a;
  6.     ra = 20;
  7.     //在语法上,这里定义个指针变量,开辟了4个字节,存储a的地址
  8.     int* pa = &a;
  9.     *pa = 20;
  10. }

从汇编来看,引用的底层也是类似指针存地址的方式进行的

引用和指针的不同点:

1. 引用在定义时必须初始化,指针最好初始化,但是不初始化也不会报错。
2. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
3. 没有NULL引用,但有NULL指针
4. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
5. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
6. 有多级指针,但是没有多级引用
7. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
8. 引用比指针使用起来相对更安全
9.引用在概念上定义一个变量的别名(没开空间),指针存储一个变量的地址 

六、内联函数

调用函数需要建立栈帧,建立栈帧要保存一些寄存器,结束后又要恢复...,这些都是有消耗的,对这些频繁调用的函数如何进行优化呢?

c++频繁调用的函数,定义成inline,会在调用的地方展开,没有栈帧的开销

c语言为了小函数避免建立栈帧的消耗-》》提供了宏函数支持,预处理阶段展开

提到宏,我们进行一个小测试,请读者自己写一个Add的宏,先不要看答案。

普遍存在的错误写法
#define Add(int x,int y) return x+y;//典型的错误写法,是宏而不是函数
#define Add(x,y) x+y; //错误 分号问题


#define Add(x,y) x+y  // 错误 优先级问题 #define Add(x,y)  (x+y)  // 错误 +的优先级大于|和&

标准写法

记住宏原理是替换,你替换一下看看对不对
#define Add(x,y) ((x)+(y))

既然c语言已经解决了,为什么c++还要提供inline函数呢?(宏函数的缺点)
a.不支持调试 b.宏函数语法复杂容易出错 c.没有类型安全的检查

  1. inline int add(int x,int y)  //直接在函数前加一个inline构成inline函数
  2. {
  3.     return x + y;
  4. }
  5. int main()
  6. {
  7.     int c = 0;
  8. }

内联函数特性

1. inline是一种以空间换时间的做法,省去调用函数额开销。所以代码很长(一般来说10行以上就算长)或者有循环/递归的函数不适宜使用作为内联函数,因为展开以后程序会变的很大。
2. inline对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline的函数体内有循环/递归等等,编译器优化时会忽略掉内联。
3. inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。

4.类里面定义的函数默认就是inline函数。

这里解释以下什么叫以空间换时间

七、auto关键字(C++11)

C++11 中,标准委员会赋予了 auto 全新的含义即: auto 不再是一个存储类型指示符,而是作为一个新的类型 指示符来指示编译器, auto 声明的变量必须由编译器在编译时期推导而得
  1. int a = 10;
  2. auto b = a;//类型声明成auto,可以根据a的类型自动推导b的类型,a是int类型,b也是int类型

eg: 

  1. int TestAuto()
  2. {
  3. return 10;
  4. }
  5. int main()
  6. {
  7. int a = 10;
  8. auto b = a;
  9. auto c = 'a';
  10. auto d = TestAuto();
  11. //typeid打印变量的类型
  12. cout << typeid(b).name() << endl;
  13. cout << typeid(c).name() << endl;
  14. cout << typeid(d).name() << endl;
  15. //auto e; 无法通过编译,使用auto定义变量时必须对其进行初始化
  16. return 0;
  17. }


注意:使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类。因此auto并非是一种类型的声明,而是一个类型声明时的占位符,编译器在编译期会将auto替换为变量实际的类型。

auto的使用细则

1. auto 与指针和引用结合起来使用, auto 声明指针类型时,用 auto auto* 没有任何区别,但用 auto 声明引用类型时则必须加 &
  1. int main()
  2. {
  3. int x = 10;
  4. auto a = &x;
  5. auto* b = &x;
  6. int& y = x;
  7. auto c = y;
  8. auto& d = x; //指定了d是x的引用
  9. //typeid打印变量的类型
  10. cout << typeid(x).name() << endl;
  11. cout << typeid(a).name() << endl;
  12. cout << typeid(b).name() << endl;
  13. cout << typeid(y).name() << endl;
  14. cout << typeid(c).name() << endl;
  15. }

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

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

auto不能推到的场景

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

 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导

2.auto不能用来推导数组

八、 基于范围的for循环(C++11)

在c++98中如果要遍历一个数组,可以按照以下方式进行

  1. int arry[] = { 1,2,3,4,8,9 };
  2. for (int i = 0; i < sizeof(arry) / sizeof(int); i++)
  3. {
  4. cout << arry[i] << " ";
  5. }

为了简化操作,C++11引入了基于范围的for循环                                                                           for循环后的括号由冒号分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围

  1. for (auto e : arry)
  2. {
  3. cout << e << " ";
  4. }

 同样也可以自己定义,但不如auto好用

  1. for (int x : arry)
  2. {
  3. cout << x << " ";
  4. }
注意:与普通循环类似,可以用 continue 来结束本次循环,也可以用 break 来跳出整个循环

如果我们想用范围for来改变数组中的值(让数组中的每一个值都加1),我们该如何做呢?

首先我们可能会简单的认为只要将e++即可

  1. int main()
  2. {
  3. int arry[] = { 1,2,3,4,8,9 };
  4. for (auto e : arry)
  5. {
  6. e++;
  7. }
  8. for (auto e : arry)
  9. {
  10. cout << e << " ";
  11. }
  12. }



但通过结果发现,数组并没有被改变,原因就是:这个范围for 是自动遍历,依次取出arry中的元素,赋值给e,直至结束,e的确实改变了,但打印时,e又被arry中的元素依次赋值,所以数组并没有被改变。

那么该如何实现呢? 我们可以用引用实现,e是arry数组中每个元素的别名,e的改变就会影响arry数组中值的改变。

  1. int main()
  2. {
  3. int arry[] = { 1,2,3,4,8,9 };
  4. for (auto& e : arry)
  5. {
  6. e++;
  7. }
  8. for (auto e : arry)
  9. {
  10. cout << e << " ";
  11. }
  12. return 0;
  13. }

 那么用指针可以吗? 答案是不行的,因为arry中数据是int,不能将int 转化为int *

范围for使用条件

范围for必须是数组名,传参的时候相当于传了数组的首地址,也就是传了指针,不符合范围for的语法规定

  

 九、指针空值nullptr(C++11)

NULL实际是一个宏,在传统的C头文件(stdio.h)中,可以看到如下代码:

  1. #ifndef NULL
  2. #ifdef __cplusplus
  3. #define NULL 0
  4. #else
  5. #define NULL ((void *)0)
  6. #endif
  7. #endif
可以看到, NULL 可能被定义为字面常量 0 ,或者被定义为无类型指针 (void*) 的常量 。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,比如
  1. void f(int a)
  2. {
  3. cout << "f(int)" << endl;
  4. }
  5. void f(int* a)
  6. {
  7. cout << "f(int*)" << endl;
  8. }
  9. int main()
  10. {
  11. f(0);
  12. f(NULL);
  13. return 0;
  14. }

 

程序本意是想通过f(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义成0,因此与程序的初衷相悖,所以引入nullptr来解决这一问题。

 注意
1. 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
2. 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
3. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/2023面试高手/article/detail/696754
推荐阅读
相关标签
  

闽ICP备14008679号