目录
前言
从C语言到C++的转变无疑是巨大的:从面向过程编程到面向对象编程......如果一门心思扑到"封装、继承、多态"上学习,恐怕学到后面就会被一些语法整的困惑不解。
本文的目的就是尽量填平C语言与C++之间隐形的坑:C++常用但C语言却没有的基础知识。
本文不仅适合于用做初学C++的入门文章,还可以帮助不清楚C与C++之间差异的读者理清思绪。

一、namespace是什么?
1.情景引入
在C语言编写的代码中,存在着大量的变量、函数。而这些变量、函数又都存储于全局作用域中,这就引出了一个问题:如果变量名与函数名相同的情况下,会导致很多冲突。
如代码:
cpp
#include<iostream>
using namespace std;
int test(int a)
{
return a;
}
int main()
{
int test = 1;
cout << test(test);
return 0;
}
(cout相当于C语言的printf,cin相当于C语言中的scanf, <<是流插入运算符,>>是流提取运算符,cout/cin不需要像printf/scanf输入输出时那样手动控制格式,C++的输入输出可以自动识别变量类型。 )
这段代码放在编译器上是编译错误的,原因也很显然:test重定义了。编译器不知道使用的test指的是函数还是变量,这就造成了命名冲突。
C语言没办法解决类似这样的命名冲突问题 ,而C++通过namespace关键字,成功解决了名称冲突的问题。
2.命名空间定义
namespace用法:
命名空间需要用到namespace关键字,后面跟命名空间的名字,然后接一对{}即可,{} 中即为命名空间的成员。
前面提到C语言将所有的变量、函数都放在全局作用域中,从而造成冲突。
C++允许程序员通过namespace关键字自定义命名空间(作用域),自定义的命名空间同全局作用域一样,也可以以命名空间中可以定义变量/函数/类型。
如:
cpp
namespace a1
{
int hello = 6;
int test(int a)
{
return a;
}
}
那么该如何使用已经定义好的命名空间呢?
3.命名空间的使用
命名空间的使用有三种方式:
①使用某变量/函数前,加空间名称与作用限定符"::".
cppint main() { cout << a1::hello; return 0; }
②使用某变量/函数前,使用using将命名空间中某个成员引入全局作用域;
cppusing a1::hello; int main() { cout << hello; return 0; }
③使用某变量/函数前,使用using namespace "命名空间名"将整个空间引入;
(谨慎使用,防止再次出现冲突问题)
cppusing namespace a1; int main() { cout << hello; return 0; }
例如如上述代码通过加入namespace命名空间就能编译通过:
明确告诉编译器,我使用的是a1空间中的函数test,未加作用域限定符的就是全局作用域中的变量test。
cpp
#include<iostream>
using namespace std;
namespace a1
{
int hello = 6;
int test(int a)
{
return a;
}
}
int main()
{
int test = 1;
cout << a1::test(test);
return 0;
}
现在再回过头看"using namespace std"是什么意思就一目了然了。
常见教材上一般直接让初学者直接使用"using namespace std",却又迟迟不说明原因,其实原因很简单:
<iostream>头文件中包含有cin、cout等标准输入输出的声明,但考虑到命名冲突的问题,于是C++将相关声明都放进了std命名空间中。如果不将std空间通过using展开,那么使用std空间里的成员就需要作用限定符,如std::cout。
实际上std中可以不止有<iostream>头文件中的声明,其他头文件如<vector><string>的声明也可以在其中。
这里还需要指出一个容易误解的区域:很多初学者误以为cin等"函数"的实现是在std中。其实不然:std的主要作用是避免名称冲突 ,防止标准库的名字(如cout
)和你自己写的代码名字(比如你定义一个cout
变量)打架。它们的定义是在C++标准库的预编译二进制文件中,std中的声明是为了方便后续的编译链接。
最后指出:
std
命名空间的内容是随着包含的头文件动态增长的,且未包含的头文件中的声明不会进入 std。
每包含一个标准库头文件(如 <vector>
),该文件就会向 std
命名空间内添加新的声明,这是 C++ 模块化设计的关键特性,也是一般教材没有细说的地方。
二、缺省参数
1.缺省参数概念
缺省参数指:
在声明或者定义函数时,为形参预备一个值,在调用该函数时,如果没有传入实参就采用该形参的缺省值,否则使用指定的实参。
这个预备值的名称叫做缺省值,有缺省值的参数叫缺省参数,有缺省参数的函数叫缺省函数。
如
cpp
#include<iostream>
using namespace std;
void fun(int a = 100)
{
cout << a;
}
int main()
{
fun();//没有传参时就用缺省值
fun(10);//传参了就用实际参数
return 0;
}
2.缺省参数细节
缺省参数可以分为全缺省参数和半却参数。
全缺省参数就是该函数的所有参数都有缺省值;
半缺省参数,指该函数的某一个参数有缺省值,但不是所有函数都有缺省值。
还有几点小细节:
①半缺省参数:若某个形参是缺省参数,那它之后的参数就必须是缺省参数;
②缺省参数不能在函数声明和定义中同时出现,防止重定义缺省值;
③缺省值必须是常量或者全局变量
C语言并不支持缺省值。
三、函数重载
1.函数重载概念
函数重载:
C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同,用来处理实现功能类似数据类型不同的问题。
注意:没有按返回值区分!
如
cpp
int add(int a, int b)
{
return a + b;
}
double add(double a, double b)
{
return a + b;
}
int main()
{
cout << add(1, 2) << endl;
cout << add(3.1, 4.2) << endl;
return 0;
}
编译器是怎么做到区分这两个重载函数的呢?
2.重载原理:C++的名字修饰规则
首先需要明确在不同的环境下,同一个程序中函数被编译后产生的名字是不一样的。
比如Linux与windows有关C++的名字修饰规则就截然不同,我们以较为清晰明了的Linux规则结合上述重定义代码为例做说明:
上述代码在Linux G++编译后形成的可执行文件中两个add函数的函数名如下,

我们看到两个add函数的函数名发生了改变,add(int ,int )型函数名,从add------>_Z3addii;add(double,double)型,从add------>_Z3adddd。
在Linux环境下C++名字修饰规则是:
前缀 :
_Z
表示 C++ 名称长度+函数名 :
5print
表示函数名参数类型编码:
i
→int
d
→double
f
→float
c
→char
P
→ 指针(如Pi
表示int*
)总结:g++的函数修饰后变成【_Z+函数长度 +函数名+类型首字母】。
注意:没有返回值!
而C语言编译后,其中的函数名就不会有这样的变化。
所有C++能实现函数重载,C语言不能的根本原因是:C++有着另一套函数名字修饰规则。
四、引用
"引"用在C++中经常使用,但在C语言中完全没有"引用"的身影。
1.引用的概念
引用概念
引用,并非定义一个新变量,而是为已有变量取一个别名。引用与被引用的变量指向同一块地址空间。
类型& 引用变量名(对象名) = 引用实体;
如
cpp
int main()
{
int a = 1;
int& A = a;
//下面两次输出完全一样
cout<<a<<endl;
cout<<A<<endl;
}
从概念上看,既然引用与被引用变量指向同一块空间,所以对它们进行操作时是同样的效果:
"A=7"等价于"a=7",都是被指向的那块空间数据被修改为"7"。
注意:引用的类型必须与被引用的变量类型一致!
2.引用的特性
1. 引用在定义时必须初始化;
2. 一个变量可以有多个引用;
3. 引用一旦引用一个实体,再不能引用其他实体;
4.常变量只能被常引用所引用;
1. 引用在定义时必须初始化;
如下述代码,编译器都会报错或者达不到预期效果
引用未初始化:
编译器报错:"error C2530: "A": 必须初始化引用"
cpp
int main()
{
int a = 1;
int& A;
return 0;
}
3. 引用一旦引用一个实体,再不能引用其他实体;
引用不可更改引用对象:
这里本意是想更改引用A的指向,使A从指向a转化到指向b。
但引用不能更改指向,所以下述代码实质上是将b变量的值赋值给了A(a)变量;
cpp
int main()
{
int a = 1, b = 2;
int& A = a;
A = b;
return 0;
}
4.常变量只能被常引用所引用;
所谓常变量,即被const修饰的变量,是不可更改的。常引用同理。
换句话说,常变量只有读权限,没有写权限,所以引用常变量的引用也必须只有读权限,而没有写权限。
cpp
int main()
{
const int b = 2;
const int& B = b;//正确
int& A = b;//错误
return 0;
}
那么普通变量能否被常引用所引用呢?
答案是肯定的,让我们分析一下:
普通变量具有可读可写的权限,而常引用仅有可读权限。如果将普通变量被常引用所引用,这是发生权限的下降,是可接受的。
所以,编译器允许引用变量对原变量的操作权限下降,而不允许权限上升。
引用变量权限<=被引用变量权限
3.引用的使用场景
1)引用传参
还记得刚接触指针时写的swap函数吗?那时形参用的是指针实现,其原理是:函数调用时将原变量的地址传入函数,通过指针直接对该地址上的数据进行操作。
同理,引用也是指向被引用变量的同一块地址,所以引用也可以实现交换函数swap:
cpp
void swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
2)引用做返回值
引用做返回值?如下所示
cpp
int& add(int a, int b)
{
int c = a + b;
return c;
}
原理也很简单,在函数内部有个int c的**局部变量,**函数用引用的形式返回c变量存储的数据。
但这里有一个十分容易引起bug的细节:
首先明确函数中的c变量是一个局部变量,当函数调用完毕后,函数所用到的所用空间都会被系统释放,以便调用其他函数。
问题在于接受函数引用返回值的变量!如果是普通变量,那还好;但如果是引用变量,则可能出现bug。
比如下代码:打印出来的是3?还是7?咋一看打印的肯定是3才对。
cpp
int& add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int& ret = add(1, 2);
add(3, 4);
cout << ret << endl;
return 0;
}
实际验证后打印出来的是7,出现bug?
原因是什么?
还记得引用的定义吗,与被引用变量指向同一块空间,可是函数中被引用的临时变量已经被释放了。如果在这个时候又调用了其他函数,并且系统将被引用的空间分配给了该函数,那么该函数是可以对被引用的空间进行修改的。
这还只是引用做返回值诸多bug的一种情况,所以:
在使用引用做返回值时需注意,如果函数返回了出来函数作用域后,返回对象还在(全局变量或者静态变量),则可以使用 引用返回,否则,则必须使用传值返回。
3)引用传值与普通传值效率比较
普通函数的参数或者返回值,是直接通过拷贝完成的。
最典型的就是普通函数(不用指针和引用)无法完成swap函数,因为传入函数内部的只是一个拷贝副本,这也就引出一个现象:如果拷贝的是某种自定义的类或数据结构,占据较大的空间,那么频繁的调用函数的过程中,频繁的拷贝则拖慢了程序的运行效率。
所以,实际编写代码的过程中,通过利用引用或者指针的方式可以提升代码的运行效率,但随之而来的又是可能的野指针或者引用做返回值时的细节问题。
4.引用与指针的区别
引用与指针的不同
引用概念上是一个变量的别名,没有内存空间;而指针存储一个变量地址,实际有地址空间。
引用在定义时必须初始化,指针没有因性要求(最好初始化,防止野指针);
引用在初始化时引用一个实体后,就不能再改变引用其他实体,而指针可以在任何时候指向任何 一个同类型实体;
没有NULL引用,但有NULL指针;
在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位下是4字节,64位下是8字节);
引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小;
有多级指针,但是没有多级引用;
访问实体方式不同,指针需要显式解引用,引用编译器自己处理;
引用比指针使用起来相对更安全;
五、内敛函数
已知在调用函数时会有时间的损耗:如函数栈的开辟等。如果一些函数实际执行时间甚至小于被调用时的损耗时间,我们就可以使用inline关键字,使得该函数成为内敛函数。
1.内敛函数概念
概念:
以inline修饰的函数叫做内联函数,编译时内联函数会在调用的地方像宏一样展开,没有函数调用建立栈帧的开销,从而程序运行的效率。
如
cpp
inline int& add(int a, int b)
{
int c = a + b;
return c;
}
2.内敛函数特性
**①内敛函数是一种典型的空间换时间做法。**但也有缺陷存在:可能会使目标文件变大,优势:少了调用开销,提高程序运行效率;
②inline函数可以看作时程序员对编译器所提出的一个建议,在实际编译过程中还是看编译器内部优化,所以:应该尽量选择内部代码少但又频繁调用,且非递归调用的函数添加inline。
③用inline关键字修饰的函数,最好函数的定义与声明放在一起。
六、auto:推导关键字
随着学习的深入,以及一些自定义数据结构的复杂,对于程序员来说一些类型可能难于拼写又或者出现与其它关键字混淆的情况,于是auto关键字的作用便日益显得有用、便利。
1.auto概念
在早期,auto的概念时:修饰的变量,是具有自动存储器的局部变量。
但在C++11中,标准委员会赋予了auto全新的含义:
auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量可以也必须由编译器在编译时期推导而得。
换句话说,就是让编译器通过上下文去推测某变量的类型,而不用编写者一次又一次的去书写。
如下代码,编译器可以通过赋值号之后的数据类型,自动推断该变量应该是什么类型。
cpp
int main()
{
auto a = 1;
auto b = 'b';
auto c = 3.2;
return 0;
}
注意:使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto 的实际类型。
2.auto细节
①用auto声明指针类型时,用auto和auto*没有任何区别,但auto声明引用类型时则必须加&;
cpp
int main()
{
int a = 1;
char b = 2;
auto* p1 = &a;
auto p2 = &a;
auto& pp = b;
return 0;
}
②用auto同一行定义多个变量时,这些变量的类型必须一样;
cpp
auto a = 1, b = 2;
auto i = 3.14 j = 2.12;
③auto不能做函数的形参;

④auto不能用来定义数组;

七、范围for
在C语言想要遍历一个数组,一般采用类似for(int i=0;i<n;i++)的写法。而C++(C++11之后)则引入了一种全新的写法:范围for
1.语法
对于一个有范围的集合而言,可以使用范围for进行遍历:
for循环后的括号由冒号" :"分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。
范围for中也支持break或者continue等规则。
示例
cpp
int main()
{
int a[] = { 1,2,3 };
for (auto i : a)
{
cout << i << ' ';
}
return 0;
}
2.细节
① for循环迭代的范围必须是确定的;
有一种情况:数组名传参时,实质上传的是数组首地址,若此时在函数内部使用范围for则会报错。如下所示
cpp
void test(int p)
{
for (int i : p)
{
//报错
}
}
②迭代的对象要实现++和==的操作。
八、为什么推荐使用nullptr?
在编程中我们定义指针变量时,若暂时用不上该指针变量,一般会通过NULL置空,但在C++我们常用nullptr代替NULL。
为什么?
实际上NULL在头文件中有两种常见定义:①define NULL 0;②define NULL (void*0);它这两种定义分别用于不同的场景,如指针变量置空时常用第二种,函数返回空时常用第一种。
可在一些场景下,这两种情况就发生了歧义:
cpp
void fun(int i)
{
cout << "fun(int i)" << endl;
}
void fun(int* i)
{
cout << "fun(int* i)" << endl;
}
int main()
{
fun(NULL);
return 0;
}
原本意是想通过传入一个空指针,调用第二种函数,但由于NULL被定义成0,结果却反而调用了第一种函数。
于是C++(C++11后)引入了nullptr关键字,用来赋值指针变量,防止出现上述二义性的情况。
总结
本文较为系统的整理介绍了八种C++与C语言的差异,指出了一些C++经常使用而C语言完全没有的基础知识,对于想要从C语言入门C++的读者,或者想要知道C++与C语言差异的读者相信本文能为你提供帮助。
整理不易,希望能帮到你。
读完点赞,手留余香~