C++是在C语言的基础上发展而来的,解决了C语言中存在的一些明显的问题。本文针对C语言存在的问题,引出C++中的解决方案,以对C++的语法进行说明和分析。
命名空间
域的概念
可以将C++中的域理解为作用域。C++标准规定:使得特定名字保持有效的那些可能并不连续的程序文本就是该名字的作用域。作用域限定了名字的可见范围和使用范围。C++中常见的域有:类域、命名空间域、局部域、全局域。
什么是命名空间
在大型的计算机程序或文档中,往往会出现数百或数千个标识符,若使用C语言完成这些程序,往往会造成命名冲突的问题,命名冲突主要体现在两点:1.与库中的标识符冲突 2.与其他协作组成员的标识符冲突。解决这一问题是命名空间存在的主要理由。
在C++中,将声明与定义组合到一个声明区域,称为命名空间。当使用namespace
关键字定义一个命名空间域时,该域可对域中的变量/函数/类型进行隔离。一个命名空间就定义了一个新的作用域,命名空间中的所有内容都局限于该命名空间中。关于命名空间,有如下几点需要注意:
- 命名空间不以分号( ; )结尾定义;
- 命名空间往往是是用来限制全局声明/定义的;
- 命名空间可以嵌套定义;
- std为C++标准库的命名空间;
- (同一项目的不同文件中)多个相同的命名空间域会被视为同一个命名空间域
namespace shr
{
//命名空间的嵌套定义
namespace shr_A
{
int a = 10;
int b = 20;
}
namespace shr_B
{
int c = 13;
int d = -21;
}
}
//与上面的命名空间一起会被视为同一个命名空间
namespace shr
{
int n; //默认为 0
int sum(int x, int y)
{
return x + y;
}
}
命名空间的使用
若要使用命名空间中的声明/定义,有如下三种方式
- 将命名空间全部展开
- 使用域作用限定符(
::
)对命名空间中的某个成员进行指定访问 - 使用
using
将命名空间中的某个成员引入
namespace shr
{
int n; //默认为 0
int sum(int x, int y)
{
return x + y;
}
}
//全部展开
using namespace shr;
//指定引入
using shr::sum;
int main()
{
//指定访问
int sum = shr::sum(2, 7);
cout << "sum = " << sum << endl;
return 0;
}
编译器的搜索顺序为:局部域->全局域->被展开或者被指定了的命名空间域。命名空间不展开/不指定时不去命名空间域进行搜索。使用命名空间时,需要注意下面几点:
- 展开命名空间相当于把域中所有的声明/定义暴露到上一级空间;
- 不建议将命名空间直接全部展开,建议使用
using
将所需的指定成员引入
缺省函数
什么是缺省函数
缺省函数的参数列表中含有缺省参数。缺省参数在声明或定义函数时为函数的参数指定一个缺省值,在调用函数时,如果没有实参则形参采用形参的缺省值,否则使用指定的实参。
缺省函数分类
缺省函数按照函数参数列表中缺省参数的数量,分为全缺省函数和半缺省函数。全缺省函数的参数列表全部为缺省参数,半缺省函数的参数列表部分、依次缺省。
//全缺省函数
void Func(int a = 10, int b = 20, int c = 30)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl;
}
//半缺省函数,缺省参数从右往左依次给出
void Func(int a, int b = 10, int c = 20)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl;
}
const int default_capa = 10;
//缺省参数的缺省值必须为常量或者全局变量
int* CreatArr(int n = default_capa)
{
int* ret = (int*)malloc(sizeof(int) * n);
return ret;
}
缺省函数的使用使模块化程序设计更加灵活,开发者可以根据自己的需要进行指定传参或直接使用参数的默认值。定义和使用缺省函数时需要注意以下几点:
- 为了避免函数传参的歧义,有多个缺省参数时,实参默认从左向右传递;
- 不能对某一个特定的缺省参数进行指定传参;
- 函数的声明和定义不能同时缺省,只需要在函数的声明中缺省(若有)。这样做是考虑到函数声明和定义中的缺省值可能不同;
- 半缺省参数必须从右往左依次给出,不能间隔给出;
- 缺省值必须是常量或者全局变量。
函数重载
什么是函数重载
C++中允许在同一作用域中声明几个功能类似的的同名函数,这些同名函数的形参列表不同,常用来处理功能类似而数据个数/类型不同的函数的问题。需要注意的是,函数的返回值不在函数重载的定义范围内,返回值不同的函数不构成重载。借助函数重载,一个函数名可以有多种用途。
形参列表不同有三种情况:
- 参数数量不同
- 参数类型不同
- 参数类型顺序不同
// 1.参数个数不同
int Sum(int a, int b)
{
return a + b;
}
int Sum(int a, int b, int c)
{
return a + b + c;
}
// 2.参数类型不同
double Sum(double x, int y)
{
return x + y;
}
// 3.参数类型顺序不同
double Sum(int x, double y)
{
return x + y;
}
C++如何支持函数重载
要清楚 C++ 如何支持函数重载,就要先了解 C 为何不支持函数重载。假设有 test.c 和 Add.c 两个文件,分别使用和定义了Add函数,在多文件编译时,test.c和Add.c会单独编译形成各自的目标文件(.o),在链接的合并段表和符号表阶段,由于test.o中的符号表中的没有Add函数的有效地址,所以会在Add.o的符号表中寻找Add函数的定义(有效地址),并将Add的临时地址替换为有效地址。在C中,函数名是直接作为符号体现在符号表中的,这就会造成一个问题:当有多个函数名相同的函数时,链接器无法判断将哪个函数地址作为所使用函数的有效地址,便会报出重定义的链接错误。
在C++中,编译器在编译时会将函数名进行修饰,函数名修饰时与函数的名称和参数列表有关,每两个函数名相同而参数列表不同的函数经过修饰后便会有不同的名称,由此,在链接阶段,链接器便能正确选择对应的函数名以实现有效地址的填充。
每个编译器的函数名修饰规则不尽相同,下面演示的是以 gcc 和 g++ 编译器为例分别对 C 和 C++ 程序编译后的函数名。
可以发现,gcc未对 test.c 中的Add函数进行函数名修饰;而经过g++对test.cpp中的Add函数名修饰后变成了[_Z+函数长度+函数名长度+参数类型首字母]
的形式,以保证后序进行链接的正确性。
了解C++支持函数重载的原理之后,需要额外明晰的是,函数的返回值类型不构成函数重载的原因与函数名修饰无关,例如,考虑这种情况:函数的参数列表都相同,仅仅只有返回值类型不同,即使将返回类型加入函数名修饰,因为调用函数时并不能明确返回值类型,所以编译器也不能到底判断调用哪个函数。
int func(double a, double b)
{
return a + b;
}
double func(double a, double b)
{
return a + b;
}
int main()
{
//此时编译器也不能判断该调用哪个函数,编译报错
cout << func(3.2, 5.7) << endl;
return 0;
}
内联函数
宏
在一个函数被调用时,总是要先开辟一块函数栈帧,而宏是直接替换的,使用宏可以避免函数调用和返回的开销。宏的使用更加灵活,节省资源,但是宏具有以下劣势:
- 宏往往导致代码可读性差,可维护性差;
- 宏的定义和使用容易出错;
- 宏不能调试;
- 宏是类型无关的,不够严谨和安全。
内联函数可以有效避免宏的这些缺点。
内联
用inline
关键字修饰的函数称为内联函数,编译时编译器会将内联函数在被调用的地方直接展开,没有函数栈帧开辟和销毁、函数返回的开销。内联函数可以提升程序运行的效率。
内联适用于短小而需要被频繁调用的函数,内联函数的使用一方面提高了程序的运行效率,另一方面可能会使源文件变大。使用内联函数需要注意以下几点:
- 不当使用内联函数,展开时会额外产生大量机器指令,使可执行文件变得臃肿;
- 事实上,内联对于编译器只是一个建议,该函数最终是否内联取决于编译器,一般情况下,较长的函数和递归函数会被编译器否决内联;
- 在 dubug 模式下,为了便于调试,默认内联不起作用;
- 内联函数是被直接展开的,不被纳入符号表,所以不要将内联函数的声明和定义(在不同文件中)分离;
- 由于内联函数不被纳入符号表,所以可以在不同的源文件中定义函数名相同但功能不同的内联函数而不会发生重定义问题。
auto关键字
类型推导
在 C++11 中,auto
是一个类型指示符。使用auto定义变量时必须进行初始化,在编译期间,编译器会根据auto
右边的表达式推导auto
的实际类型。因此auto
是一种类型声明时的占位符,编译器在编译期间会将auto
替换为变量实际的类型。在实际使用中,可以依此实现对长类型的替换。
auto的使用有如下细则:
- 用 auto 声明指针类型时,auto和auto*没有区别,但引用必须使用auto&;
- 使用 auto 在同一行定义多个变量时,这些变量必须是同一类型的,否则编译器会报错。编译器是根据第一个变量推导 auto 的类型的,并以此类型定义后面的变量;
- auto不能作为函数的参数;
- auto不能用来声明数组。
基于范围的for循环(range for)
对于一个有范围的集合,循环的范围对开发者而言往往是多余的,有时甚至会产生一些错误。因此C++中引入了基于范围的 for 循环,for循环的括号中由冒号( : )分为两部分,第一部分为用于迭代的元素,第二部分为被遍历的集合(范围)。
void test(void)
{
int arr[] = { 1,3,5,7,2,4,6,8 };
for (int e : arr) {
cout << e << ' ';
}
}
将 auto 关键字与基于范围的 for 循环结合是个常见的用法。
void test(void)
{
double arr[] = { 3.2,3.14,5.23,7.15,2.75,4.15,6.42,8.0 };
for (auto e : arr) {
cout << e << ' ';
}
}
基于范围的 for 循环有以下使用条件:
- for循环迭代的范围必须是确定的;
- 迭代的对象要实现 ++ 和 == 操作;
- range for 的底层是正向迭代器(iterator),所以被迭代的容器必须具有迭代器,并且不能逆向迭代。
nullptr关键字
在C语言中所用的 NULL
指针其实是一个宏,它在C头文件(stddef.h)中的定义如下:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
可见 NULL 可能被定义为字面常量 0,也可能被定义为无类型指针的常量(void*)0,在使用指针空值时,会不可避免地遇到一些麻烦。在 C++98 中,字面常量 0 默认是一个整型常量,若要将其当做空指针使用,则要进行强制类型转换(void*)0。
为了解决上述问题,同时要实现语言的向前兼容性,C++新定义了一个关键字nullptr
,相当于(void*)0。在C++11中,sizeof(nullptr)
与 sizeof((void*)0)
所占的字节数相同。
int main()
{
cout << typeid(NULL).name() << endl; //int
cout << typeid(nullptr).name() << endl; //std::nullptr_t
cout << sizeof(nullptr) << endl; //8(64 bit)
cout << sizeof((void*)0) << endl; //8(64 bit)
cout << sizeof(NULL) << endl; //4
return 0;
}