目录
- 1. 什么是C++
- 2. 命名空间
- 2.1 命名空间的定义
- 2.2 命名空间的使用
- 3. 输入和输出
- 4. 缺省参数
- 4.1 概念
- 4.2 分类
- 5. 函数重载
- 5.1 函数重载概念
- 5.2 为什么支持函数重载
- 6. 引用
- 6.1 概念
- 6.2 特性
- 6.3 常引用
- 6.4 指针与引用的区别
- 7. 内联函数
- 7.1 特性
1. 什么是C++
C语言是结构化和模块化的语言,适合处理较小规模的程序。对于复杂的问题,规模较大的程序,需要高度的抽象和建模时,C语言则不合适。为了解决软件危机,
20世纪80年代,计算机界提出了OOP(object oriented
programming:面向对象)思想,支持面向对象的程序设计语言应运而生。 1982年,Bjarne
Stroustrup博士在C语言的基础上引入并扩充了面向对象的概念,发明了一种新的程序语言。为了表达该语言与C语言的渊源关系,命名为C++。因此:C++是基于C语言而产生的,它既可以进行C语言的过程化程序设计,又可以进行以抽象数据类型为特点的基于对象的程序设计,还可以进行面向对象的程序设计。
简言之C++是由C语言发展而来,弥补和扩展了C语言中不足的设计和功能。
2. 命名空间
在C/C++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。
在C语言中,当一个全局变量的变量名与其它变量的变量名或者函数名发生冲突时,这种情况是C语言除了更改它们的名字之外没有其它处理方法了。
#include <stdio.h>
#include <stdlib.h>
int rand = 10;
int main()
{
printf("%d\n", rand);
return 0;
}
// 编译后后报错:error C2365: “rand”: 重定义;以前的定义是“函数”
因此c++为了解决上述情况,便引入命名空间的概念。
C++官方定义的命名空间为:std
。
C++标准库中的函数或者对象都是在命名空间std中定义的,所以要使用标准函数库中的函数或对象都要使用std来限定。
2.1 命名空间的定义
定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,然后接一对{}即可,{} 中即为命名空间的成员。
//pri是命名空间的名字,一般开发中是用项目名字做命名空间名。
//1. 正常的命名空间定义
namespace pri
{
// 命名空间中可以定义变量/函数/类型
int rand = 10;
int x = 20;
//...
int Add(int left, int right)
{
return left + right;
}
struct Node
{
struct Node* next;
int val;
};
}
//2. 命名空间可以嵌套
namespace N1
{
int a;
int b;
int Add(int left, int right)
{
return left + right;
}
namespace N2
{
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;
}
}
namespace N1
{
int Add(int left, int right)
{
return left + right;
}
}
//会将两个N1合成一个
注意:一个命名空间就定义了一个新的作用域,也叫做命名空间域,命名空间中的所有内容都局限于该命名空间。
这里有个问题,域中的变量是局部还是全局变量?
其实域中定义的变量是全局变量,与普通的全局变量没有什么区别,但是会影响编译器的查找规则。
编译器的默认查找规则是:在查找该变量时首先会在局部域中寻找(函数内部),若找不到在去全局域中去找,还找不到直接报错,它并不会在去命名空间中去找。
那么如果就是想访问命名空间中的内容时该怎么做呢?
2.2 命名空间的使用
接下来简单介绍几种访问命名空间中成员的方法:
#include <iostream>
namespace pri
{
int g_val = 10;
//...
}
第一种访问命名空间做法:
int main()
{
//名称pri + 域作用限定符::
printf("%d\n", pri::g_val);
return 0;
}
这种写法是以指定的方式直接去访问命名空间中的成员,若找不到也不会去局部或者全局区去找,直接报错。因此即使不同的命名空间域中出现了相同的成员时,使用各自的名称+域作用限定符即可访问各自的空间中的成员,不会出现冲突。
但是这种要求指定的写法每次都要写上空间名称+域作用限定符才能访问其中的成员,那么有没有一种简单的写法呢?
第二种访问命名空间做法,展开命名空间:
//使用如下写法后续即可直接访问空间中的成员
using namespace pri
//展开命名空间
int main()
{
printf("%d\n", g_val);
return 0;
}
命名空间就好比一堵围墙,把其中的成员封装起来与外界进行隔离,使用特定的方式才能访问其中的成员,以达到解决命名冲突的问题。
而上面这种做法就相当于把这堵墙给拆了,谁都可以直接进行访问也比较方便,但是这种做法又会出现最开始的问题,出现命名冲突。
有没有一种方法即能方便的使用又能不展开命名空间的方法呢?
第三种访问命名空间做法,指定部分展开:
//将最常用的部分成员展开
//比如官方定义的空间std中的输出函数cout
//换行字符endl
using std::cout;
using std::endl;
//自己定义空间中的常用成员
using pri::g_val;
int main()
{
printf("%d\n", g_val);
cout << g_val << endl;
//不展开写法:
//std::cout << pri::g_val << std::endl;
return 0;
}
这种写法很好的解决了即能方便的使用部分成员又能不展开全部的命名空间,但需要注意的是要避免自己定义的与常用重名即可。
3. 输入和输出
#include<iostream>
using namespace std;
int main()
{
int a;
char b;
//这里的cin和cin分别是C++中的对象
//负责输入和输出
//会自动识别类型
cin >> a >> b;
cout << a << ' ' << b << endl;
return 0;
}
- 使用cout标准输出对象(控制台)和cin标准输入对象(键盘)时,必须包含< iostream >头文件以及按命名空间使用方法使用std。
- cout和cin是全局的流对象,endl是特殊的C++符号,表示换行输出,他们都包含在包含头文件中。
- <<是流插入运算符,>>是流提取运算符。
- 实际上cout和cin分别是ostream和istream类型的对象,>>和<<也涉及运算符重载等知识,这些知识后续才会学习,所以这里只是简单学习它们的使用。后面还有一个章节更深入的学习IO流用法及原理。
和C中的输入输出函数相比各有各的优势,建议是根据场景,哪个好用用哪个。
4. 缺省参数
4.1 概念
缺省 = 默认,含义相同
缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。
//不传参数默认a=0
void Func(int a = 0)
{
cout << a << endl;
}
int main()
{
// 没有传参时,使用参数的默认值,输出0
Func();
// 传参时,使用指定的实参,输出10
Func(10);
return 0;
}
如果函数带参数且不使用缺省值,调用时不传参数会直接报错。
4.2 分类
- 全缺省参数
void func(int a = 10, int b = 20, int c = 30)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl;
}
int main()
{
func();
func(a);
func(a, b);
func(a, b, c);
return 0;
}
上面几种调用方式都是正确的,但需要注意的是传入的参数与形参是从左到右依次匹配的,不可以跳着传参。
错误调用方式:
func(a, ,c);
func(, b, c);
- 半(部分)缺省参数
void func(int a, int b = 10, int c = 20)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl;
}
int main()
{
func(a);
func(a, b);
func(a, b, c);
return 0;
}
至少要传入一个参数,同时也是从左到右依次匹配。
错误调用方式:
func();
func(a, ,c);
func(, b, c);
关于半缺省还要几点需要注意:
- 半缺省参数必须从右往左依次来给出,不能间隔着给。
- 缺省参数不能在函数声明和定义中同时出现,同时出现会出现二义性,一般要在声明中给出。
- 缺省值必须是常量或者全局变量
- C语言不支持(编译器不支持)
5. 函数重载
自然语言中,一个词可以有多重含义,人们可以通过上下文来判断该词真实的含义,即该词被重载了。
比如:以前有一个笑话,国有两个体育项目大家根本不用看,也不用担心。一个是乒乓球,一个是男足。前者是“谁也赢不了!”,后者是“谁也赢不了!
5.1 函数重载概念
函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同,常用来处理实现功能类似但数据类型不同的问题,比如:
#include<iostream>
using namespace std;
// 1、参数类型不同
int Add(int left, int right)
{
cout << "int Add(int left, int right)" << endl;
return left + right;
}
double Add(double left, double right)
{
cout << "double Add(double left, double right)" << endl;
return left + right;
}
// 2、参数个数不同
void f()
{
cout << "f()" << endl;
}
void f(int a)
{
cout << "f(int a)" << endl;
}
// 3、参数类型顺序不同
void f(int a, char b)
{
cout << "f(int a,char b)" << endl;
}
void f(char b, int a)
{
cout << "f(char b, int a)" << endl;
}
int main()
{
Add(10, 20);
Add(10.1, 20.2);
f();
f(10);
f(10, 'a');
f('a', 10);
return 0;
}
以下代码也构成函数重载,因为参数个数不同,但是有什么问题?
#include <iostream>
using namespace std;
void func(int a = 0)
{
cout << "func(a)" << endl;
}
void func()
{
cout << "func(NULL)" << endl;
}
int main()
{
func();
return 0;
}
这种调用方式会出现问题,因为全缺省参数调用时也可以不用传参,因此就会和无参的函数引起冲突,即出现二义性,使得函数调用不明确。
- 返回值不同构不构成函数重载?
答案是不构成,因为在调用时编译器无法确定返回值类型,进而无法分辨出是要调用哪个函数。这样便出现了二义性,所以不构成函数重载。
5.2 为什么支持函数重载
C++对于函数重载是支持的但是C语言却不支持是为什么?
简单的说是:C语言源程序在编译期间生成符号表时,函数是直接用它的函数名+地址的形式形成的符号表,没有任何的修饰。相同的函数会形成相同的符号和地址,但是符号表中的符号都是只存在一个,因此会编译错误,所以C语言不支持函数重载。
而C++则使用了不同的策略,C++源程序在编译形成符号表的时候,函数的符号表并不是单单地将函数名作为符号,而是使用了某种规则,这个规则叫做函数名修饰规则。
将它的参数(个数或类型或类型顺序)信息也加入到了符号中,因此相同的函数名有着不同的参数时经过修饰后最终得到的符号是不同的,不同的符号会配上不同的地址,所以后续进行链接时会根据调用的函数的参数(个数或类型或类型顺序)信息,去符号表里就能找到对应地址处的函数。这也就是为什么C++支持函数重载。
其实在编译器的角度,编译时就已经把它们当作不同的函数来看待了。
6. 引用
6.1 概念
引用不是新定义一个变量,而是给已存在变量取了一个别名,在语法层面编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间,但是在编译器层面是要开辟空间的。
比如:李逵,在家称为"铁牛",江湖上人称"黑旋风"。
示例:
int main()
{
int a = 10;
//类型& 引用变量名(对象名) = 引用实体;
int& ra = a;
//结果相同
printf("%p\n", &a);
printf("%p\n", &ra);
return 0;
}
此时的ra与a一起表示一块存储空间,修改其中一个另一个也会发生变化,和指针有些许类似。别名也可以有很多个。
有了引用后,有些方面就会比指针更方便,比如交换两个数据:
void swap(int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
}
int main()
{
int a = 10;
int b = 20;
swap(a, b);
return 0;
}
x是a的别名,y是b的别名,交换x和y就是交换a和b。比传地址方便多了,这是引用的一个便捷之处。
6.2 特性
- 引用在定义时必须初始化。
//错误示例
int main()
{
//error
int& ra;//必须初始化,也就是必须要说明你是谁的引用
return 0;
}
- 一个变量可以有多个引用。
int main()
{
int a = 10;
int& ra = a;
int& rra = ra;
return 0;
}
- 用一旦引用一个实体,再不能引用其他实体。
int main()
{
int a = 10;
int& ra = a;
int b = 20;
ra = b;
//注意上面一条语句的含义是把b的值赋给ra也就是a
//并不是让ra重新引用作为b的别名
//也就是说一旦引用了一个实体,就无法在引用其它实体
return 0;
}
6.3 常引用
void TestConstRef()
{
int a = 10;
//这里定义的a可读可写
int& ra = a;
//正确,这种写法属于权限不变,也可读可写
const int& rra = a;
//正确,这种写法属于权限缩小,可读不可写
const int b = 10;
//这里定义的b可读不可写
int& rb = b;
//报错,这种写法属于权限放大,可读可写
const int& rrb = b;
//正确,这种写法属于权限不变,也可读不可写
}
当const修饰一个引用或者指针时,对于引用或者指向对象的读写权限,只可不变或者缩小,绝不可方放大。
int cnt()
{
int x = 10;
return x;
}
int main()
{
double d = 12.34;
int& rd = d;
//该语句编译时会出错,类型不同
//当类型不同时会发生隐式类型转换,转换的过程中会发生临时拷贝
//因此本质rd是引用的是这个拷贝的临时变量
//而临时变量具有长属性,所以前面要加上const修饰
int& ret = fun();
//这种写法也是错误的
//因为接收的返回值并不是x,而是x的拷贝
//将x的值拷贝到一个临时变量中返回由ret来接收
//又因为临时变量具有长属性
//所以前面也需要加const修饰
return 0;
}
6.4 指针与引用的区别
引用和指针的不同点:
- 引用概念上定义一个变量的别名,指针存储一个变量地址
- 引用在定义时必须初始化,指针没有要求
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
- 没有NULL引用,但有NULL指针
- 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址所占字节个数(32位平台下占4个字节)
- 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
- 有多级指针,但是没有多级引用
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
- 引用比指针使用起来相对更安全
7. 内联函数
根据之前对栈帧的理解,调用函数是有成本的,体现在时间和空间上,具体是栈帧的形成和释放有成本。
对于某些需要频繁调用的函数,也就意味着需要不断地开辟和释放栈帧,这样对于程序的效率是非常不利的。
因此为了提高运行效率,对于此类频繁调用的函数C语言的做法是使用宏函数来代替,因为宏在预处理时就在它所在的位置替换成它的宏体代码,并不会开辟栈帧空间。这种做法有效的提高了效率,但是宏本身是有一些缺陷的,比如:
- 宏体无法调试
- 宏直接进行替换,并不会进行类型检查
- 容易写错,尤其是优先级问题
- 等等…
而在C++中为了对解决上面提到的一些缺陷,便提出了一个新的关键字:inline。
它的作用是:以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率,可以理解为宏的升级版。
语法如下:
//与普通写法没什么区别
//只需要在返回值前加inline即可
inline int Add(int x, int y)
{
return x + y;
}
int main()
{
int ret = Add(1, 3);
cout << ret << endl;
return 0;
}
汇编角度下:
可以发现并没有发生函数调用,直接在寄存器中进行运算,并把结果保存到ret中。
如果不加inline修饰:
call指令为函数调用,可以发现不加的情况下发生了函数调用开辟栈帧。
7.1 特性
这里思考一个问题,被inline修饰的函数都会展开吗?
答案是并不是,如果只要被inline修饰就会展开的话,就会造成文本代码的篇幅增大,如果调用的地方很多,展开后文件就会变的越大。
所以建议用inline来修饰需要频繁调用的小函数,不能是递归或者大函数,如果是编译器会忽略inline特性。
而且inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,下图为《C++prime》第五版关于inline的建议:
为什么函数长了以后就不展开了?
原因是函数太长展开后会引起代码膨胀。
比如说一个函数编译后有30条指令,而实际调用它的地方有10000处,在每个调用处展开后指令条数为30*10000条。而不展开的话就只有那一份30条的指令,每次调用只是去复用那块代码,因此实际指令数为30+10000条。
又因为编译出来的可执行程序的大小也就是安装包的大小与指令条数是息息相关的,从上面可以得知,展开与不展开后的指令条数相差是非常大的,那也就会导致安装包的大小也会相差的非常大,安装包太大会比较占用存储空间。
内联的一些特性:
- inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运行效率。
- inline不建议声明和定义分离,分离会导致调用时链接错误。因为inline被展开,就没有函数地址了,链接就会找不到,所以建议直接在头文件中或者本文件中定义。