当前位置:   article > 正文

从C语言到C++过渡篇(快速入门C++)

从C语言到C++过渡篇(快速入门C++)

目录

引言

命名空间

C++ 的输入输出(cout & cin)

输出 cout

输入 cin

缺省参数

函数重载

知识要点讲解

函数重载底层

引用&

内联函数

auto &  nullptr

结语


引言

很多同学从C语言到C++的转变不知从何下手,今天这篇文章主要是介绍相对于 C语言 C++新增的各种小语法

我们可以这么去想:首先,创建新语言的人都是旧语言中的大佬,所以我们本贾尼博士的初衷并不是创建一门新语言,只是单纯的地想对C语言进行改进,结果改进着改进着,就创建出了C++

我们在过渡章节学习的是一些本贾尼博士对C语言改进的小语法,而后是类和对象、内存管理、模板等等。当然,你也可以将今天学习的内容理解为本贾尼博士对C语言的吐槽与改进

而我们今天要讲的知识点依次为:

  • 命名空间
  • C++输入与输出(cout  &&  cin)
  • 缺省参数
  • 函数重载
  • 引用
  • 内联函数
  • auto 关键字
  • 区别于C语言的空指针(nullptr)

命名空间

由于C++是C语言的改进版本,所以C++是兼容C的,也就是说:C语言的大部分代码都能在cpp文件中跑得了

首先我们先来看一个程序:

  1. #include<stdio.h>
  2. int rand = 3;
  3. int main()
  4. {
  5. printf("%d\n", rand);
  6. return 0;
  7. }

事实证明,目前这个程序是能跑得过的

但是,如果我们多包含一个头文件的话,那么就过不了了

  1. #include<stdio.h>
  2. #include<stdlib.h>
  3. int rand = 3;
  4. int main()
  5. {
  6. printf("%d\n", rand);
  7. return 0;
  8. }

我们会看到,只是添加了一个  #include<stdlib.h>  就过不了了

这是因为 rand 是stdlib 库里面的一个库函数,也就是说,我们自己定义了一个整形rand,库里面也有一个rand函数,而我们的系统在编译的时候,会先将 test.c 这个文件先转换成 test.i 文件,而这个文件会将头文件里面的内容展开,也就相当于你命名冲突了(你可以这么理解)

我们不能改库里面的函数,所以我们就只能改我们自己的变量名

但是但是但是,我们以后出去做项目的时候,基本上都是好几个人几十个人共同做同一个项目,这时,假设有两个程序员A和B,程序员A和B都想把rand改为randi,都想用randi这个名字

但是是多个人一起做项目,所以用同一个名字必然会报错

那我们怎么办,作为程序员连起个变量名字都要低声下气的,这哪能行?

所以有这么一个办法,就是今天晚上咱上天台打一架,谁赢了谁就用这个变量(doge)

本贾尼博士也是深受其害,所以就发明了 命名空间 namesapce 

不是有两个程序员A和B吗?那就各自建立自己的命名空间,互不干扰,如下:

  1. #include<stdio.h>
  2. #include<stdlib.h>
  3. namespace A
  4. {
  5. int rand = 3;
  6. }
  7. namespace B
  8. {
  9. int rand = 5;
  10. }
  11. int main()
  12. {
  13. printf("%d\n", rand);
  14. return 0;
  15. }

此时就不会有重命名的问题,但是当我们运行代码的时候,会发现很有意思的东西:

这打印出来的,是一个随机值

只是因为:编译器不会擅自进入命名空间内部,所以这里打印出来的rand,其实是指向rand函数的一个指针,所以我们换成打印地址可能会更好一点

但是如果这两个程序员都叫做A呢?两个命名空间都叫做A,那就重名了,是不是又得打一架啊?

这也是有解决方法的:namespace是支持嵌套的,如下:

  1. namespace Boy
  2. {
  3. namespace A
  4. {
  5. int rand = 3;
  6. }
  7. }
  8. namespace Girl
  9. {
  10. namespace B
  11. {
  12. int rand = 5;
  13. }
  14. }

上面我们说到,编译器默认是不会进入命名空间里面去查找的

这里我们就需要谈一谈编译器里的默认查找规则了

首先,编译器会先在局部域里面找,如果局部域里面找不到,那就到全局域中去找

由于test.c 文件编译成 test.i 文件时会展开头文件,所以头文件中的库函数也可以理解成是全局变量

如果在全局域中还是找不到的话,就会报错

我们这么来理解,你现在正在厨房里面做饭,这是你发现葱少了,那你就先到自己家的地里面找一找有没有葱(局部域)

如果自己家的地里面没有的话,那就在没人的野地里找找看有没有(全局域)

你不会一开始就直接悄咪咪地溜到隔壁老张家的地里面去拔两根葱(命名空间域)

这样子讲的话,也许你就明白得差不多了

这时如果我们要访问命名空间内部的整形变量n(假设该命名空间名字为hjx)的话,有三种方法:

  1. hjx::n,这就相当于你要葱,然后你提前去和隔壁家的张大爷说了一声,然后才去拔的葱
  2. using hjx::n,这就相当于隔壁家张大爷在自家菜地上立了一块牌子,说我家的葱太多了吃不完,如果有需要的可以直接来拿,但是别的什么卷心菜,菠菜什么的都不能拿
  3. using namespace hjx,这就相当于隔壁张大爷是个大好人,在自家才地上立个牌子说:大家都是邻居,我家菜地里的菜大家随便拿

由此我们可以知道我们日常写的 using namespace std; 其实就是放开了一个命名空间的使用权限,这个命名空间里面包含的是 #include<iostream>(这是一个C++的库)

但是 using namespace 直接展开命名空间也并没有想象中那么好,我们来看看下面这一段代码:

  1. #include<stdio.h>
  2. namespace Boy
  3. {
  4. int Add(int a, int b)
  5. {
  6. return a + b;
  7. }
  8. }
  9. using namespace Boy;
  10. int Add(int a, int b)
  11. {
  12. return a + b;
  13. }
  14. int main()
  15. {
  16. int a = Add(2, 5);
  17. return 0;
  18. }

且看,我们原本定义了一个函数Add,但是我们定义的 namespace 空间里面也有一个函数 Add 

当我们展开了命名空间之后,编译器会先在局部域中找Add函数,找不到之后到全局域中,由于我们使用了 using namespace Boy,所以编译器最后会到我们展开的域中去找

结果编译器发现,展开的命名空间和全局域中都有函数Add,又不符合函数重载规则(后面的知识点),所以就报错

并且,当一个命名空间内有很多内容的话,我们将其一键展开了之后,那么整个程序也会随之变大,如果我们只需要使用其中一到两个变量的话,我们就可以使用下面的方法:

using 域名::域内变量;

  1. #include<stdio.h>
  2. namespace Boy
  3. {
  4. int y = 0;
  5. int n = 3;
  6. }
  7. using Boy::n;
  8. int main()
  9. {
  10. int a = n;
  11. printf("%d\n", a);
  12. return 0;
  13. }

C++ 的输入输出(cout & cin)

由于我们的C++兼容C,所以如果喜欢接着用原来C语言那一套输入输出的话,也可以用,但是C++新增了一套新的输入输出

输出 cout

cout 中的 c,就是console——控制台(就是平时显示结果的那个黑框框),而cout中需要另外使用一个符号<<,这个符号叫流插入运算符

而流插入就是将要输入的东西全部流到控制台中

  1. #include<iostream>
  2. int main()
  3. {
  4. int a = 1;
  5. double b = 1.5;
  6. cout<<a <<b <<" "<<endl;
  7. }

cout最大的特点就是自动识别,无论你要输入的是什么类型的,都能直接流到console处,显示出来

但是如果你直接按上面的方式写的话,编译器会报错,说不认识cout和endl

这是因为C++标准库为了防止程序员定义的名字和库中的命名发生冲突,所以就将库中的种种库函数全部包含在一个命名空间之中,这个命名空间的名字就叫做 std

如上的cout、endl,包括后面要讲的cin都被包含在名为std的命名空间之内

由于编译器寻找是先局部,后全局,不会主动到命名空间之内找,所以如果我们要使用的话,我们就需要指定,比如:using namespace std;

输入 cin

cin你也可以理解为是console in

他和C语言中的scanf不一样的地方在于,他不需要取地址,也不需要写得那么麻烦,同时也可以自动识别输入的数据类型

但是cin需要使用另一个符号>>

  1. int a;
  2. double b;
  3. cin>> a >> b;

缺省参数

我们先来看下面这一段代码:

  1. #include<iostream>
  2. using namespace std;
  3. void Func(int a = 1, int b = 2, int c = 3)
  4. {
  5. cout << a << " " << b << " " << c << endl;
  6. }
  7. int main()
  8. {
  9. Func(5);
  10. Func(5, 6);
  11. Func(7, 8, 9);
  12. return 0;
  13. }

各位且看,我们在函数的参数部分,给每一个参数都赋了一个值

我们以三种不同的方式调用了同一个函数三次,最后都能得出结果

这就是缺省参数

如果我们对已经赋有缺省值的参数传了参的话,那么参数的值就是传过去的值

如果没有传参的话,那么参数的值就是缺省值

如上的类型是全缺省函数,与之对应的是半缺省函数,如下:

只给一个参数缺省值是半缺省,只给两个也是半缺省,只要还有每赋缺省值的变量,那么该函数就是半缺省函数

但是缺省值不能跳着传

这种传值方式是不被允许的,我们只能从左往右按顺序传

所以,缺省值也需要从右往左赋,如果不这样做的话,那么就意味着后面有需要传参的数,而我们又不能跳着传参,所以我们前面赋了缺省值的变量就都得传参,这样的话缺省值就失去了原有的意义

另外,缺省函数不能在声明与定义同时出现!!

如果声明定义中都有缺省函数,恰巧给的值不一样,那么编译器就无法确定缺省值是哪一个

函数重载

知识要点讲解

在谈论函数重载是什么之前,我们先来看这样一串代码:

  1. #include<iostream>
  2. using namespace std;
  3. int Add(int a, int b)
  4. {
  5. return a + b;
  6. }
  7. double Add(double a, double b)
  8. {
  9. return a + b;
  10. }
  11. int main()
  12. {
  13. return 0;
  14. }

两个重名的函数,由于参数的一些不同,所以并不影响编译结果的成功

这样子的情况,就叫做函数重载

我们能看到,上面我们定义了两个名字相同的函数Add,但是由于参数的不同,所以这个代码能跑得了,这就是函数重载的一个体现

但是函数重载必须是在同一片域里面的才能叫做重载,试想一下:一个函数在全局域中,一个函数在命名空间里面,这两个函数即使名字相同,甚至参数都相同,也能跑得了,这是因为两者并不在同一片域里面

打一个形象的比喻:隔壁张大爷和王大爷家里面都有养鸡,张大爷跟大家说过了,想吃鸡蛋的可以到他家里面去拿,而他家里面有乌鸡的蛋,或者另外品种鸡的蛋,但本质上都是鸡

但是王大爷家里面的鸡蛋不给拿呀,即使王大爷家里面有和张大爷家里面一样的鸡蛋,那我们也不能去拿

可能例子不是很恰当,但是如果不是在同一片域里面的话,那么涉及到的就是命名空间相关的知识了,而非现在我们要讲的函数重载的相关知识点

接下来我们来讲一讲函数重载的各种类型:

  • 参数个数不同的可以重载
  • 参数类型有不同的可以重载
  • 参数顺序不同的也可以重载

如下代码:

  1. //参数个数不同
  2. int B(int a, int b)
  3. {
  4. cout << a << b << endl;
  5. }
  6. int B(int a)
  7. {
  8. cout << a << endl;
  9. }
  10. //函数参数类型不同
  11. int Add(int a, int b)
  12. {
  13. return a + b;
  14. }
  15. double Add(double a, double b)
  16. {
  17. return a + b;
  18. }
  19. //参数顺序不同
  20. void A(int a, char b)
  21. {
  22. cout << a << b << endl;
  23. }
  24. void A(char b, int a)
  25. {
  26. cout << a << b << endl;
  27. }

这里有几个需要注意的点:

  1. 函数的返回值不同,并不是函数重载的条件之一,一个为int,一个为char,但如果参数相同,那么就不能构成函数重载
  2. 函数参数名不同并不能作为函数重载的条件
  3. 函数参数有无缺省值并不能作为函数重载的条件,如果两个函数的参数类型相同且个数一样的话,那么这两个函数就不会构成重载,而会报错
  4. 一个函数是全缺省,另一个函数是无参,本质上是函数重载,但是调用时会产生歧义,如下代码:
  1. #include<iostream>
  2. using namespace std;
  3. void test(int a = 1, int b = 2)
  4. {
  5. cout << 1;
  6. }
  7. void test()
  8. {
  9. cout << 1;
  10. }
  11. int main()
  12. {
  13. test();
  14. return 0;
  15. }

虽然构成函数重载,但是按上述代码所示去调用函数的话,那么编译器就不知道到底要调用哪个函数,因为两个函数都不用传参也能调用,这时就会报错

所以理论上构成函数重载,但是现实中用不了

函数重载底层

我们写完了程序之后,编译器会经过编译与链接这个步骤,接着才是我们看到的结果,这里我们就详细说说编译过程:

写完了代码之后,系统会先将我们写的.cpp文件转换成.i文件,这个过程简单来说,就是检查语法,并将宏定义处理与头文件展开的一个过程

接着是将.i文件转换成.s文件,这个过程就是将我们所写代码转换成汇编代码的一个过程

最后是将.s文件转换成.o或.obj文件,这个过程就是将汇编代码转换成机器看得懂的二进制代码,也就是0和1

最后.o或.obj文件会转化成.exe可执行文件,这个过程就是链接了,我们就不在这里展开

我们以后出去工作的时候,经常会碰到声明与定义分离的情况,或者里可以理解成要头文件和.cpp文件一起定义

这时,.h文件会单独一个线程转换成.o文件,.cpp又是另一个线程转换成.o文件

但是一个.o文件没有另一个.o文件的地址,需要到另一个.o文件中去找

这时,就会涉及到找地址的规则

为什么C语言不支持函数重载,这是因为C语言找地址是直接通过函数名去找的,如果有两个一样的函数名的话,地址不一样就会报错

但是C++支持函数重载,这时因为祖师爷深受其害,所以给找.o文件的过程改了一下,还是按照函数名去找地址,但是到了.o文件的时候,函数名也会被重新命名,所以原本一样的函数名,由于参数的不同(顺序、个数、类型),所以转换之后的名字也不同,所以找到的地址也就不同,这也是为什么C++支持函数重载,如下图分别为C语言和C++:

C语言

C++

引用&

引用的本质,就是取别名

举一个例子:水浒传中有这么一号人物,林冲,又称豹子头,也称天雄星

林冲的别名是豹子头,那我们说豹子头其实就是在说林冲,豹子头就是林冲的别名

再比如李逵,黑旋风,在家乡时被叫做铁牛

你问铁牛吃饭没,不就是在问李逵吃没吃饭吗,铁牛和黑旋风都是李逵的别名

回到我们的C++

我们创建了一个整形变量a,创建出来之后我们同样可以给这个变量取一个别名,就叫b吧

  1. #include<iostream>
  2. using namespace std;
  3. int main()
  4. {
  5. int a = 1;
  6. int& b = a;
  7. cout << a << " " << b << endl;
  8. b++;
  9. cout << a << " " << b << endl;
  10. return 0;
  11. }

如上,我们对变量a取了一个别名b,当我们对b++的时候,a也跟着++了

这是因为b本来就是a,也就是你问铁牛吃饭没和问李逵吃饭没问的是一个人

我们来看一段C语言的程序:

  1. void Swap(int a, int b)
  2. {
  3. int tmp = a;
  4. a = b;
  5. b = tmp;
  6. }
  7. int main()
  8. {
  9. int a = 1, b = 2;
  10. Swap(a, b);
  11. return 0;
  12. }

如果放在C语言里面,这样子是无法完成交换的,因为形参是实参的一份临时拷贝,交换的只是形参,对实参没有影响

但如果我们用上引用呢?

  1. #include<iostream>
  2. using namespace std;
  3. void Swap(int& a, int& b)///
  4. {
  5. int tmp = a;
  6. a = b;
  7. b = tmp;
  8. }
  9. int main()
  10. {
  11. int c = 1, d = 2;
  12. Swap(c, d);
  13. cout << c << " " << d << endl;
  14. return 0;
  15. }

我们可以看到,是能完成交换的,这是因为上述代码中a是c的别名,也就是说a就是c,另一个参数同理,所以我们传参的时候也无需取地址

我们再来看一看指针的引用:

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

我们可以看到,在上述代码中我们创建了一个指针变量pr,给pr取了一个别名pa

我们这么理解这段代码呢?

有这么一块空间里面存了a变量,而pr是一个指向这一块空间的指针,pa是pr的别名,本质上就是pr,当我想要将pa置为NULL的时候,pr也会被跟着置为NULL

  1. #include<iostream>
  2. using namespace std;
  3. int main()
  4. {
  5. int a = 1;
  6. int* pr = &a;
  7. int*& pa = pr;
  8. pa = NULL;
  9. printf("%p\n%p", pa, pr);
  10. return 0;
  11. }

我们之前在实现链表的时候,二级指针那一块是学的很难受的,这时很多人会选择看书

但是有的书会觉得大家指针学的不太好,就偷偷使用了引用的

  1. void ListPushBack(LTNode** plist, int a);
  2. void ListPushBack(LTNode*& plist, int a);

现在我们就能看得懂这段代码是什么意思了,就是对一级指针的一个取别名而已,我们传参的时候也不用传&plsit,直接传plist即可

这里有三个需要注意的点:

1. 引用前需要初始化

    我们不能说我先取个二狗的别名,不知道给谁,等会儿看谁像二狗就给谁,这是不允许的

2. 引用可以引用多个

    我们可以这么去理解:一个人能被取多个绰号,而在编程中,一个变量也能有多个别名

3. 引用了一个变量之后,就不能再做更改。就好比我们给张三起了个绰号叫大马猴,不能说看着别人更像大马猴就不叫他大马猴,把他的绰号给别人,这是不被允许的

接着我们来详细讲一讲引用中权限的相关问题(const)

我们先来看这样一段代码:

  1. const int x = 1;
  2. int& y = x;

这段代码是编不过的,这是因为我们的const修饰了变量x之后,就意味着x不能改变,可读不可写

但是我们给x取别名的时候,取的类型却是可读可写

这种情况叫做权限的放大,是不被允许的

  1. const int x = 1;
  2. const int& y = x;

当我们将程序改编成这种情况的时候,才能编得过,将一个可读不可写的变量取一个可读不可写的别名,这种情况叫做权限的平移

当然,还有下面这种情况:

  1. int x = 1;
  2. const int& y = x;

我们定义的变量x是可读可写的,我给他取一个可读不可写的变量,这是被允许的,因为我本来的x是可以被改变的,而我这时给这个变量起个别名,让这个别名不能做更改,你可以理解成别人说你可以随地吐痰,但是我们有教养做不出这种缺德事,自我约束一个道理

这种情况叫做权限的缩小,是被允许的

再来拓展一个:

  1. const int* p1 = &x;
  2. int*& p2 = p1;

如上这种情况,其实是错误的

这是因为我们第一条代码的const修饰的其实是*p1,也就是p1指向的那个地址,也就代表着p1指向的那个地址不可改变,而p1可以改变指向

但是我们第二条代码却是给p1取了一个别名p2,而p1是一个指针,里面装着的是x的地址,我们给这个指针变量取了一个别名就相当于给一个可读不可写的变量起了一个可读可写的别名,或者说在一个不能随地吐痰的地方你随地吐痰,这是不被允许的

  1. const int* p1 = &x;
  2. const int*& p2 = p1;

当我们将代码改成这样时,才是正确的

其实在const中还有一个知识点,与隐式类型转换有关

  1. double i = 3.14;
  2. int& a = i;

上述代码是错误的,并不是隐式类型转换出了问题,而是一个权限放大的问题

你想啊,我们隐式类型转换之后,转换完后的那个值在哪里呢?

答案是会被存在一个编译器临时创建的空间里,而这时转换过后的值具有常性,意味着可读不可写

而这时我们给其取一个可读可写的别名,这就是权限的放大

  1. double i = 3.14;
  2. const int& a = i;

这时我们的代码就编得过了

而引用除了做参数之外,还有做返回值的情况

但是做返回值是在类和对象章节里才会出涉及到的知识点,和类和对象中的拷贝构造函数有关

当一个类创建了一个对象的时候,如果要传参的话,以内置类型作为返回值的话,就会调用拷贝构造函数,如果拷贝构造函数没有以引用作为唯一参数的话,那么就会引发无限循环,直至程序挂掉

这些知识点就是后话了,我们在前面这些章节里也没法讲,所以我们就进入下一个知识点吧

引用在语法上是不开空间的,我们日常与人交谈或是被问到的时候,都会认为引用是不开空间的

但是其实,引用的语法在底层和指针是一样的,也会开空间,但是日常聊起时就说这是不开空间的,就当作是一个不成文的规定吧

最后我们来说一说引用与指针有什么区别:

  1. 指针存的是一个变量的地址,引用就是给一个变量取别名
  2. 引用必须初始化,指针不必
  3. 指针在指向了一个变量之后,指针的指向可以更改,但是引用不能
  4. 没有空引用,但是有空指针
  5. sizeof时,引用的大小是取别名变量的大小,而指针就是指针本身的大小:4/8字节
  6. 引用加一就是引用的变量的值加一,指针则是指向的地址加一(往后移一个地址)
  7. 有多级指针,但是没有多级引用
  8. 引用相对指针会更加安全

内联函数

内联是C++定义的一个新概念

当我们平常有一些会被频繁调用的小函数的时候,我们就可以使用内联

总所周知,我们平常调用函数都是要开辟栈帧的,但是如果我们有一个小函数要经常调用时,也就意味着我们要面临频繁的开辟栈帧,这对程序的运行效率是相当不友好的

祖师爷也是深受其害,所以就搞出了内联这么一个东西

内联其实就是在函数的前面加一个inline,如下:

  1. inline int Add(int a, int b)
  2. {
  3. return a + b;
  4. }

当编译器碰到内联函数的时候,在编译时就会将其直接在函数中展开

各位想想,如果我们将其从函数中展开了,那么就意味着我们不需要开辟栈帧了,这样子机会节省很多时间,大大提高效率

但是,内联的本质只是一个建议,一个给编译器的建议

试想,如果,我们有一个函数,有100行,调用10000次

如果我们展开的话,就要调用100*10000次

但是如果不展开的话,我们就只用调用100+10000次,两边的效率是完全不一样的

所以,内联只适合频繁调用的小函数,而且,这只是一个给编译器的建议

另外,内联不支持声明和定义分离

这是因为我们内联其实就是将函数展开,当我们的.cpp文件变成.o文件去找地址时,函数已经被展开成代码段了,是没有地址的,找不到,所以不支持声明定义分离

auto &  nullptr

auto其实就是一个关键字

auto的其中一个用法就是自动识别,如下:

  1. int a = 0;
  2. auto b = a;
  3. auto c = 3;
  4. auto d = &a;
  5. auto* e = &a;

auto关键字是能够实现自动识别的

但是auto不能作为函数参数,也不能声明数组,这是规定

但是auto有一个用法还不错:

  1. for(int i=0;i<sizeof(arr)/sizeof(int);i++)
  2. {
  3. //......
  4. }
  5. for(auto e:arr)
  6. {
  7. //......
  8. }

这其实就是将arr中的值依次不断拿出来并赋值给e,这样子写会方便很多

nullptr

这是C++新创建的,由于C语言中的NULL存在一定的安全问题,所以祖师爷就创建了一个nullptr供大家使用

各位以后可以习惯一下使用nullptr而非NULL

结语

本篇文章是关于C语言像C++过渡的

当这里面的知识都掌握了之后就可以着手去学习类和对象了

看到这里,如果觉得对你有帮助的话,希望能多多支持!!!

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

闽ICP备14008679号