一.C++发展历史
C++的起源可以追溯到1979年,当时BjarneStroustrup(本贾尼·斯特劳斯特卢普)在⻉尔实验室从事计算机科学和软件⼯程的研究⼯作。⾯对项⽬中复杂的 软件开发任务,特别是模拟和操作系统的开发⼯作,他感受到了现有语⾔(如C语 ⾔)在表达能⼒、可维护性和可扩展性⽅⾯的不⾜。
1983年,BjarneStroustrup在C语⾔的基础上添加了⾯向对象编程的特性,设计出了C++语⾔的雏形,此时的C++已经有了类、封装、继承等核⼼概念,为后来的⾯向对象编程奠定了基础。这⼀年该语⾔被正式命名为C++。
在随后的⼏年中,C++在学术界和⼯业界的应⽤逐渐增多。⼀些⼤学和研究所开始将C++作为教学和研究的⾸选语⾔,⽽⼀些公司也开始在产品开发中尝试使 ⽤C++。这⼀时期,C++的标准库和模板等特性也得到了进⼀步的完善和发展。C++ 的标准化⼯作于1989年开始,并成⽴了⼀个ANSI和ISO国际标准化组织的联合标准化委员会。
1994年标准化委员会提出了第⼀个标准化草案。在该草案中,委员会在保持斯特劳斯特卢普最初定义的所有特征的同时,还增加了部分新特征。在完成C++ 标准化的第⼀个草案后不久,STL是惠普实验室开发的⼀系列软件的统称。它是由 AlexanderStepanov、MengLee和DavidRMusser在惠普实验室⼯作时所开发出来 的。
在通过了标准化第⼀个草案之后,联合标准化委员会投票并通过了将STL包含到C++标准中的提议。STL对C++的扩展超出C++的最初定义范围。虽然在标准中 增加STL是个很重要的决定,但也因此延缓了C++标准化的进程。
1997年11⽉14⽇,联合标准化委员会通过了该标准的最终草案。1998年,C++的ANSI/IS0标准被投⼊使⽤。
二.C++和C
C++兼容C的大部分C的大部分语法,很多C的程序在C++环境下一样可以运行。C++中需要把定义⽂件代码后缀改为.cpp,vs编译器看到是.cpp就会调⽤C++编译器编译,linux下要⽤g++编译,不再是gcc。
include<stdio.h>
int main()
{
printf("hello world");
return 0;
}
这样的c程序在c++环境下当然可以正常运行,不过c++也有自己的一套语法。
include<iostream>
using namespace std;
int main()
{
cout<<"hello world\n"<<endl;
return 0;
}
三.命名空间(namespace)
1.命名空间的意义
在运行一个程序时,你是否也会因习惯或疏忽而导致命名冲突?刚开始学习时我们可能对此没有太多感觉,但当成千上万行代码时,这种情况也许并不少见。所以我们在c++中引入namespace关键字,⽬的是对标识符的名称进⾏本地化,以避免命名冲突或名字污染。
2.namespace的定义
- 定义命名空间,需要使⽤到namespace关键字,后⾯跟命名空间的名字,然后接⼀对{}即可,{}中即为命名空间的成员。命名空间中可以定义变量/函数/类型等。
- namespace本质是定义出⼀个域,这个域跟全局域各⾃独⽴,不同的域可以定义同名变量。
- C++中域有函数局部域,全局域,命名空间域,类域;域影响的是编译时语法查找⼀个变量/函数/类型出处(声明或定义)的逻辑,所有有了域隔离,名字冲突就解决了。局部域和全局域除了会影响编译查找逻辑,还会影响变量的⽣命周期,命名空间域和类域不影响变量⽣命周期。
- namespace只能定义在全局,当然他还可以嵌套定义。
namespace a
{
int n = 1;
namespace b{
int c = 2;
}
}
- 项⽬⼯程中多⽂件中定义的同名namespace会认为是⼀个namespace,不会冲突。
eg:多个文件里都有叫aabb的namespace,当多个文件合并,多个aabb也会自己合并。 - C++标准库都放在⼀个叫std(standard)的命名空间中,我们常会见到这样一句代码:
using namespace std;
作用我们在下面讲。
3.namespace的使用
在使用namespace时,我们绕不开一个符号, ::
(域作用限定符)。
比如:
using namespace kkk
{
int a = 1;
}
int a = 2;
int main()
{
printf("%d\n", a);
printf("%d\n", kkk::a);
return 0;
}
在上述代码中,第一行输出2,第二行输出1。我们可知:
- ::左边不带域名,则在程序会在全局里找。
- ::左边带域名,则会去左边的域里找。
在了解完::
的使用,我们使⽤命名空间中定义的变量/函数,有三种⽅式:
- 指定命名空间访问,项⽬中推荐这种⽅式。
- using将命名空间中某个成员展开,项⽬中经常访问的不存在冲突的成员推荐这种⽅式。
- 展开命名空间中全部成员,项⽬不推荐,冲突⻛险很⼤,⽇常⼩练习程序为了⽅便推荐使⽤。(也就是我们常见的
using namespace std
)
四.C++输⼊&输出
- iostream是InputOutputStream的缩写,是标准的输⼊、输出流库,定义了标准的输⼊、输出对象。
- std::cin 是istream类的对象,它主要⾯向窄字符的标准输⼊流。
- std::cout 是ostream类的对象,它主要⾯向窄字符的标准输出流。
cout一行可输入输出多个,它会自动识别类型。
cout<< a <<" "<<endl; 这个代码正确
cout<<a " " <<endl; 这个代码错误
流插入实质上是一个函数调用,他只允许一个参数,所以第一行代码错误。
流输出也是同理。
- std::endl 是⼀个函数,流插⼊输出时,相当于插⼊⼀个换⾏字符加刷新缓冲区。endl相当于一个函数put了一个换行符。
- <<是流插⼊运算符,>>是流提取运算符。(C语⾔还⽤这两个运算符做位运算左移/右移)
- cout/cin/endl等都属于C++标准库,C++标准库都放在⼀个叫std(standard)的命名空间中,所以要通过命名空间的使⽤⽅式去⽤他们。⼀般⽇常练习中我们可以
using namespace std
,实际项⽬开发中不建议using namespace std
。
五.缺省参数
缺省参数顾名思义,就是声明或定义函数时为函数的参数指定⼀个缺省值。若调用函数时传了参数,则使用传的参数,若没传实参,则用默认的缺省值。
eg:
void Func(int a = 0)
{
cout << a << endl;
}
int main()
{
Func();//打印出0,没有传参时,使⽤参数的默认值
Func(1);//打印出1,传参时,使⽤指定的实参
return 0;
}
缺省参数也分为全缺省和半缺省参数,顾名思义,全缺省就是所有参数都给一个缺省值,半缺省则是有的参数有缺省值有的参数没有。
tips:
-
函数声明和定义分离时,缺省参数在声明中给,定义里不给,不然当定义和声明里给的缺省值不一样,则会报错
-
C++规定半缺省必须从右往左缺省,
void func (int a, int b = 1)
这种形式不可 -
带缺省参数的函数调⽤,C++规定必须从左到右依次给实参,不能跳跃给实参
六.函数重载
C++⽀持在同⼀作⽤域中出现同名函数,但是要求这些同名函数的形参不同,可以是参数个数不同或者类型不同。这样C++函数调⽤就表现出了多态⾏为,使⽤更灵活。C语⾔是不⽀持同⼀作⽤域中出现同名函数的。
重载也分几种情况:
1. 参数类型不同
#include<iostream>
using namespace std;
int Add (int a,int b)
{
cout << "int Add(int a, int b)" << endl;
return a + b
}
double Add(double a,double b)
{
cout << "double Add(double a, double b)" << endl;
return a + b
}
函数重载不考虑返回值类型,因为调⽤时也⽆法区分
2.参数个数不同
void f(int c)
{
cout << "f(int c)" << endl;
}
void f(int a,int b)
{
cout << "f(int a,int b)" << 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;
}
tips:
void f1 ()
{
cout << "f()" << endl;
}
void f1(int a = 1)
{
cout << "f(int a)" << endl;
}
//这种属于第二种情况,参数个数不同,构成重载。
但f()调用的时候会报错,存在歧义。
编译器会不知道用第一个函数还是第二个函数
七.引用
1.引⽤的概念和定义
如同人在社会上,往往伴随着不少外号,比如豹子头林冲,及时雨宋江。引用其实也是,他不是新定义一个变量,而是给已存在的变量取一个别名。,编译器不会为引⽤变量开辟内存空间,它和它引⽤的变量共⽤同⼀块内存空间。
类型& 引⽤别名=引⽤对象
(C++中为了避免引⼊太多的运算符,会复⽤C语⾔的⼀些符号,⽐如前⾯的<<和>>,这⾥引⽤也和取地址使⽤了同⼀个符号&,⼤家注意使⽤⽅法⻆度区分就可以。)
#include<iostream>
using namespace std;
int main()
{
int a = 0;
int& b = a;
int& c = a; //b和c都是a的别名
int& d = c; //也可给c取别名,也相当于a的别名
return 0;
}
2.引⽤的特性
- 引⽤在定义时必须初始化
int a = 0;
int& ra //编译报错“ra”必须初始化引⽤
int& b = a; //正确
- ⼀个变量可以有多个引⽤
- 引⽤⼀旦引⽤⼀个实体,再不能引⽤其他实体。
int a = 1;
int& b = a;
int c = 2;
b = c;
// 这⾥并⾮让b引⽤c,因为C++引⽤不能改变指向,
这里是一个赋值,相当于此时b也就是a的值从1变成了2
3.引用的使用
- 引⽤在实践中主要是于引⽤传参和引⽤做返回值中减少拷⻉提⾼效率和改变引⽤对象时同时改变被引⽤对象。
- 引⽤传参跟指针传参功能是类似的,引⽤传参相对更⽅便⼀些
- 引⽤和指针在实践中相辅相成,功能有重叠性,但是各有特点,互相不可替代。C++的引⽤跟其他语⾔的引⽤(如Java)是有很⼤的区别的,除了⽤法,最⼤的点,C++引⽤定义后不能改变指向,Java的引⽤可以改变指向。
4.const引用
- const引用注意,权限可以缩小,但不可以放大,如:
const int a = 20;
int& b = a; //不行,这里将a的权限放大了,会报错
const int& b = a; //可以
int c = 20;
const int& rc = c; //可以,这里是缩小b的权限
//c++也等同于rc++,但不可直接给rc++
- 对于一些如
int a = 30
的常量,我们不能直接int& b = a
,但加上const就可以:const int& b = a
.对于具有常性的对象,我们往往要使用const引用。 - 在C++中,临时对象就是编译器需要⼀个空间暂存表达式的求值结果时临时创建的⼀个未命名的对象,⽽C++规定临时对象具有常性。所以不仅是常量,临时对象也需要使用const引用。
int a = 10;
const int& ra = a; //可以
int& b = a * 3; //编译报错: “初始化”: ⽆法从“int”转换为“int &”
const int& rb = a*3; //编译通过
//一个表达式如a * 3 得到的是一个临时对象,所以要用const
double c = 12.11;
int& rc = c; // 编译报错:“初始化”: ⽆法从“double”转换为“int &”
const int& rd = d; //编译通过
//这里进行了强制类型转换,将double转化成int型,在类型转换中会产⽣临时
对象存储中间值,也就是临时对象,所以要用const
5.指针和引⽤的关系
- 语法概念上引⽤是⼀个变量的取别名不开空间,指针是存储⼀个变量地址,要开空间。
- 引⽤在定义时必须初始化,指针建议初始化,但是语法上不是必须的。
- 引⽤在初始化时引⽤⼀个对象后,就不能再引⽤其他对象;⽽指针可以在不断地改变指向对象。
- 引⽤可以直接访问指向对象,指针需要解引⽤才是访问指向对象。
- sizeof中含义不同,引⽤结果为引⽤类型的⼤⼩,但指针始终是地址空间所占字节个数(32位平台下占4个字节,64位下是8个字节)
- 指针很容易出现空指针和野指针的问题,引⽤很少出现,引⽤使⽤起来相对更安全⼀些。
八.内联函数inline
- ⽤inline修饰的函数叫做内联函数,编译时C++编译器会在调⽤的地⽅展开内联函数,这样调⽤内联函数就需要建⽴栈帧了,就可以提⾼效率
- inline对于编译器⽽⾔只是⼀个建议,也就是说,你加了inline编译器也可以选择在调⽤的地⽅不展开,不同编译器关于inline什么情况展开各不相同,因为C++标准没有规定这个。inline适⽤于频繁调⽤的短⼩函数,对于递归函数,代码相对多⼀些的函数,加上inline也会被编译器忽略。
- C语⾔实现宏函数也会在预处理时替换展开,但是宏函数实现很复杂很容易出错的,且不⽅便调试,C++设计了inline⽬的就是替代C的宏函数。
- inline不建议声明和定义分离到两个⽂件,分离会导致链接错误。因为inline被展开,就没有函数地址,链接时会出现报错。
- 由于涉及到汇编语言,且前期对于inline函数没有过多需求,我们就先不展开说了。
九.nullptr
nullptr实际上就相当于c语言中的NULL,那为什么在c++中我们要换掉NULL而使用nullptr呢。实际上,NULL是一个宏,在传统的C头⽂件(stddef.h)中,可以看到如下代码:
我们可以看到,所以C++中NULL可能被定义为字⾯常量0,或者C中被定义为⽆类型指针(void*)的常量。但不管被定义成什么,在使⽤空值的指针时,都不可避免的会遇到⼀些⿇烦,比如:
using namespace std;
void f(int x)
{
cout << "f(int x)" << endl;
}
void f(int* ptr)
{
cout << "f(int* ptr)" << endl;
}
int main()
{
f(0);
f(NULL);
//本想通过f(NULL)调⽤指针版本的f(int*)函数,但是由于
NULL被定义成0,调⽤了f(int x),因此与程序的初衷相悖。
f((int*)NULL); //这样不行,会报错
f(nullptr); //成功调用void f (int * ptr)函数
return 0;
}
所以在c++中,我们引⼊nullptr,nullptr是⼀个特殊的关键字,nullptr是⼀种特殊类型的字⾯量,它可以转换成任意其他类型的指针类型。用nullptr定义空指针可以避免类型转换的问题,因为nullptr只能被隐式地转换为指针类型,⽽不能被转换为整数类型。