Lesson01–C++入门
2.命名空间
在C/C++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作
用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字
污染,namespace关键字的出现就是针对这种问题的。
2.1 命名空间定义
定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,然**后接一对{}**即可,{}中即为命名空间的成员。
//1. 普通的命名空间
namespace N1 // N1为命名空间的名称
{
// 命名空间中的内容,既可以定义变量,也可以定义函数
int a;
int Add(int left, int right)
{
return left + right;
}
}
//2. 命名空间可以嵌套
namespace N2
{
int a;
int b;
int Add(int left, int right)
{
return left + right;
}
namespace N3
{
int c;
int d;
int Sub(int left, int right)
{
return left - right;
}
}
}
//3. 同一个工程中允许存在多个相同名称的命名空间
// 编译器最后会合成同一个命名空间中。
namespace N1
{
int Mul(int left, int right)
{
return left * right;
}
}
- 命名空间中的内容,既可以定义变量,也可以定义函数
- 命名空间可以嵌套
- 同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中。
- 一个命名空间就定义了一个新的作用域,命名空间中的所有内容都局限于该命名空间中。
命名空间的三种使用方式
- 加命名空间名称及作用域限定符
int main()
{
printf("%d\n", N::a);
return 0;
}
- 使用using将命名空间中成员引入
using N::b;
int main()
{
printf("%d\n", N::a);
printf("%d\n", b);
return 0;
}
- 使用using namespace 命名空间名称引入
using namespce N;
int main()
{
printf("%d\n", N::a);
printf("%d\n", b);
Add(10, 20);
return 0;
}
- 使用cout标准输出(控制台)和cin标准输入(键盘)时,必须包含< iostreanm >头文件以及std标准命名空间。
注意:早期标准库将所有功能在全局域中实现,声明在.h后缀的头文件中,使用时只需包含对应头文件即可,后来将其实现在std命名空间下,为了和C头文件区分,也为了正确使用命名空间,规定C++头文件不带.h;旧编译器(vc 6.0)中还支持<iostream.h>格式,后续编译器已不支持,因此推荐使用**< iostreanm >+std**的方式。
- 使用C++输入输出更方便,不需增加数据格式控制,比如:整形–%d,字符–%c
4.缺省参数
4.1 缺省参数的概念
缺省参数是声明或定义函数时为函数的参数指定一个默认值。在调用该函数时,如果没有指定实参则采用该
默认值,否则使用指定的实参
4.2缺省参数的分类
1. 全缺省参数
void TestFunc(int a = 10, int b = 20, int c = 30) { //参数全部都有默认参数
cout<<"a = "<<a<<endl;
cout<<"b = "<<b<<endl;
cout<<"c = "<<c<<endl;
}
2. 半缺省参数
void TestFunc(int a, int b = 10, int c = 20) {
cout<<"a = "<<a<<endl;
cout<<"b = "<<b<<endl;
cout<<"c = "<<c<<endl;
}
// 1.半缺省参数必须从右往左依次来给出,不能间隔着给
// 2.缺省参数不能在函数声明和定义中同时出现
//a.h
void TestFunc(int a = 10);
// a.c
void TestFunc(int a = 20)
{}
// 注意:如果生命与定义位置同时出现,恰巧两个位置提供的值不同,那编译器就无法确定到底该用那个缺省值
// 3.缺省值必须是常量或者全局变量
// 4.C语言不支持(编译器不支持),是C++支持
5.函数重载
5.1 函数重载概念
函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的==同名函数、不同参数列表==,这些同名函数的形参列表(参数个数 或 类型 或 顺序)必须不同,常用来处理实现功能类似数据类型不同的问题,这样做减少了函数名的数量,避免了名字空间的污染,对于程序的可读性有很大的好处
5.2 名字修饰
运行程序的经历:预处理、编译、汇编、链接
Name Mangling是一种在编译过程中,将函数、变量的名称重新改编的机制,简单来说就是编译器为了区分
各个函数,将函数通过某种算法,重新修饰为一个全局唯一的名称。
C语言的名字修饰规则非常简单,只是在函数名字前面添加了下划线。比如,对于以下代码,在最后链接时就
会出错:
int Add(int left, int right);
int main()
{
Add(1, 2);
return 0;
}
编译器报错:error LNK2019: 无法解析的外部符号 _Add,该符号在函数 _main 中被引用。
上述Add函数只给了声明没有给定义,因此在链接时就会报错,提示:在main函数中引用的Add函数找不到函数体。从报错结果中可以看到,C语言只是简单的在函数名前添加下划线。因此当工程中存在相同函数名的函数时,就会产生冲突。
C++要支持函数重载,命名空间等,使得其修饰规则比较复杂
int Add(int left, int right);
double Add(double left, double right);
int main()
{
Add(1, 2);
Add(1.0, 2.0);
return 0;
}
error LNK2019: 无法解析的外部符号 "double cdecl Add(double,double)" (?Add@@YANNN@Z)
error LNK2019: 无法解析的外部符号 "int __cdecl Add(int,int)’’(?Add@@YAHHH@Z)
被重新修饰后的名字中包含了:函数的名字以及参数类型。这就是为什么函数重载中几个同名函数要求其参数列表不同的原因。只要参数列表不同,编译器在编译时通过对函数名字进行重新修饰,将参数类型包含在最终的名字中,就可保证名字在底层的全局唯一性。
拓展学习:C/C++函数调用约定和名字修饰规则
[C++的函数重载](
函数重载可以分为两个问题:
- 声明/定义重载函数时,是如何解决命名冲突的?(抛开函数重载不谈,using就是一种解决命名冲突的方法,解决命名冲突还有很多其它的方法)
- 当我们调用一个重载的函数时,又是如何去解析的?(即怎么知道调用的是哪个函数呢)
为什么需要函数重载
- 试想如果没有函数重载机制,如在C中,你必须要这样去做:为这个print函数取不同的名字,如print_int、print_string。这里还只是两个的情况,如果是很多个的话,就需要为实现同一个功能的函数取很多个名字,如加入打印long型、char*、各种类型的数组等等。这样做很不友好!
- 类的构造函数跟类名相同,也就是说:构造函数都同名。如果没有函数重载机制,要想实例化不同的对象,那是相当的麻烦!
- 操作符重载,本质上就是函数重载,它大大丰富了已有操作符的含义,方便使用,如+可用于连接字符串等!
编译器如何解决命名冲突的 ?
命名机制——将一个重载函数的签名映射到一个新的标识。映射为作用域+返回类型+函数名+参数列表
重载函数的调用匹配
用函数名调用的时候是如何去解析的?为了估计哪个重载函数最适合,需要依次按照下列规则来判断:
- 精确匹配:参数匹配而不做转换,或者只是做微不足道的转换,如数组名到指针、函数名到指向函数的指针、T到const T;
- 提升匹配:即整数提升(如bool 到 int、char到int、short 到int),float到double
- 使用标准转换匹配:如int 到double、double到int、double到long double、Derived到Base、T到void、int到unsigned int;
- 使用用户自定义匹配;
- 使用省略号匹配:类似printf中省略号参数
如果在最高层有多个匹配函数找到,调用将被拒绝(因为有歧义、模凌两可)。定义太少或太多的重载函数,都有可能导致模凌两可
这时侯编译器就会报错,将错误抛给用户自己来处理:通过显示类型转换来调用等等(如f2(static_cast<int *>(0),当然这样做很丑,而且你想?
编译器是如何解析重载函数调用的?
首先找出同名的一些候选函数,然后从候选函数中找出最符合的,如果找不到就报错。
编译器在对重载函数调用进行处理时,由语法分析、C++文法、符号表、抽象语法树交互处理,交互图大致如下:
步骤:
- 由匹配文法中的函数调用,获取函数名;
- 获得函数各参数表达式类型;
- 语法分析器查找重载函数,符号表内部经过重载解析返回最佳的函数
- 语法分析器创建抽象语法树,将符号表中存储的最佳函数绑定到抽象语法树上
重载函数解析大致可以分为三步:
- 根据函数名确定候选函数集
- 从候选函数集中选择可用函数集合
- 从可用函数集中确定最佳函数,或由于模凌两可返回错误
下面进行详细解释:
根据函数名确定候选函数集
根据函数在同一作用域内所有同名的函数,并且要求是可见的(像private、protected、public、friend类)。“同一作用域”也是在函数重载的定义中的一个限定,如果不在一个作用域,不能算是函数重载
即内层作用域的函数会隐藏外层的同名函数!同样的派生类的成员函数会隐藏基类的同名函数。这很好理解,变量的访问也是如此,如一个函数体内要访问全局的同名变量要用“::”限定。
为了查找候选函数集,一般采用深度优选搜索算法:
step1:从函数调用点开始查找,逐层作用域向外查找可见的候选函数
step2:如果上一步收集的不在用户自定义命名空间中,则用到了using机制引入的命名空间中的候选函数,否则结束
在收集候选函数时,如果调用函数的实参类型为非结构体类型,候选函数仅包含调用点可见的函数;如果调用函数的实参类型包括类类型对象、类类型指针、类类型引用或指向类成员的指针,候选函数为下面集合的并:
- (1)在调用点上可见的函数;
- (2)在定义该类类型的名字空间或定义该类的基类的名字空间中声明的函数;
- (3)该类或其基类的友元函数;
确定可用函数
可用的函数是指:函数参数个数匹配并且每一个参数都有隐式转换序列。
- (1)如果实参有m个参数,所有候选参数中,有且只有 m个参数;
- (2)所有候选参数中,参数个数不足m个,当前仅当参数列表中有省略号;
- (3)所有候选参数中,参数个数超过 m个,当前仅当第m + 1个参数以后都有缺省值。如果可用
集合为空,函数调用会失败。
确定最佳匹配函数
确定可用函数之后,对可用函数集中的每一个函数,如果调用函数的实参要调用它计算优先级,最后选出优先级最高的。如对《3、重载函数的调用匹配》中介绍的匹配规则中按顺序分配权重,然后计算总的优先级,最后选出最优的函数。
还有一篇文章几乎都是精华,就直接粘贴链接了
C/C++ 函数调用约定
5.3 extern“C”
有时候在C++工程中可能需要将某些函数按照C的风格来编译,在函数前加extern “C”,意思是告诉编译器,将该函数按照C语言规则来编译。
extern "C" int Add(int left, int right);
int main()
{
Add(1,2);
return 0;
}
链接时报错:error LNK2019: 无法解析的外部符号**_Add**,该符号在函数 _main 中被引用
由报错信息可以看到函数Add以C风格来编译了
6.引用
6.1应用概念
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。引用类型必须和引用实体是同种类型的。常常用在函数调用的时候,传值和传引用的问题
类型& 引用变量名(对象名)=引用实体;
void TestRef()
{
int a = 10;
int& ra = a;//<====定义引用类型
printf("%p\n", &a);
printf("%p\n", &ra);
}
6.2 引用特征
-
引用在定义时必须初始化
-
一个变量可以有多个引用
-
引用一旦引用一个实体,再不能引用其他实体
6.3 常引用
当引用常量时,相同引用类型之前要加上const
void TestConstRef()
{
const int a = 10;
//int& ra = a; // 该语句编译时会出错,a为常量
const int& ra = a;
// int& b = 10; // 该语句编译时会出错,b为常量
const int& b = 10;
double d = 12.34;
//int& rd = d; // 该语句编译时会出错,类型不同
const int& rd = d;
}
6.4 使用场景
做函数的形参和返回值
如果函数返回时,离开函数作用域后,其栈上空间已经还给系统,因此不能用栈上的空间作为引用类型返回。如果以引用类型返回,返回值的生命周期必须不受函数的限制(即比函数生命周期长)。
6.5 传值、传引用的效率比较
以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。那指针和引用作为参数和返回值类型的效率呢?引用和指针在作为传参以及返回值类型上效率几乎相同。
6.6 引用和指针的区别
在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。
在底层实现上实际是有空间的,因为引用是按照指针的方式来实现的。
引用和指针的不同点:
-
引用在定义时必须初始化,指针没有要求
-
引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
-
没有NULL引用,但有NULL指针
-
在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
-
引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
-
有多级指针,但是没有多级引用
-
访问实体方式不同,指针需要显式解引用,引用编译器自己处理
-
引用比指针使用起来相对更安全
7.内联函数
7.1概念
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数压栈的开销,内联函数提升程序运行的效率。在编译期间编译器会用函数体替换函数的调用。
7.2特性
-
inline是一种以空间换时间的做法,省去调用函数额开销。所以代码很长或者有循环****/****递归的函数不适宜使用作为内联函数。
-
inline对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline的函数体内有循环/递归等等,编译器优化时会忽略掉内联。
-
inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。
8.auto关键字(C++11)
8.1auto简介
在早期C/C++中auto的含义是:使用auto****修饰的变量,是具有自动存储器的局部变量,但遗憾的是一直没有
人去使用它,大家可思考下为什么?
C++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,aut声明的变量必须由编译器在编译时期推导而得。
int TestAuto()
{
return 10; }
int main()
{
int a = 10;
auto b = a;
auto c = 'a';
auto d = TestAuto();
//auto e; 无法通过编译,使用auto定义变量时必须对其进行初始化
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
cout << typeid(d).name() << endl;
return 0;
}
使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种 类型 的声明,而是一个类型声明时的占位符,编译器在编译期会将auto替换为变量实际的类型。
8.2 auto的使用细则
- auto与指针和引用结合起来使用
用*auto声明指针类型时,用auto和auto没有任何区别,但用auto声明引用类型时则必须加&
int main()
{
int x = 10;
auto a = &x;
auto* b = &x;
auto& c = x;
cout << typeid(a).name() << endl;
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
*a = 20;
*b = 30;
c = 40;
return 0; }
- 在同一行定义多个变量
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
void TestAuto()
{
auto a = 1, b = 2;
auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同
}
8.3auto不能推导的场景
-
auto不能作为函数的参数(应为编译器不发对其实际类型进行推导)
-
auto不能直接用来声明数组
-
为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法
-
auto在实际中最常见的优势用法就是跟以后会讲到的C++11提供的新式for循环,还有lambda表达式等进行配合使用。
-
auto不能定义类的非静态成员变量(暂不做讲解,后面讲)
-
实例化模板时不能使用auto作为模板参数(暂不做讲解,后面讲)
9.基于范围的for循环(C++)
void TestFor()
{
int array[] = { 1, 2, 3, 4, 5 };
for(auto& e : array)
e *= 2;
for(auto e : array)
cout << e << " ";
return 0; }
- for循环迭代的范围必须是确定的
对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的
方法,begin和end就是for循环迭代的范围。
void TestFor(int array[]) //会报错,因为数组范围不确定
{
for(auto& e : array)
cout<< e <<endl;
}
10.指针空指nullptr(C++11)
NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。
为了考虑兼容性,C++11并没有消除常量0的二义性,C++11给出了全新的nullptr表示空值指针。C++11为什么不在NULL的基础上进行扩展,这是因为NULL以前就是一个宏,而且不同的编译器厂商对于NULL的实现可能不太相同,而且直接扩展NULL,可能会影响以前旧的程序。因此:为了避免混淆,C++11提供了nullptr,即:nullptr代表一个指针空值常量。nullptr是有类型的,其类型为nullptr_t,仅仅可以被隐式转化为指针类型,nullptr_t被定义在头文件中:
typedef decltype(nullptr) nullptr_t;
1. 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
2. 在C++11中,sizeof(nullptr)与sizeof((void*)0)所占的字节数相同。
3. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。
本文引入了两篇大佬的文章,再放一次链接
C/C++ 函数调用约定
[C++的函数重载](