第一章 C++入门
C++简介
什么是C++
C语言是面向过程式的语言,在处理小规模的问题时则能体现出其简单易上手的的优势,但是在面对大型程序或需要高度抽象化的程序时,C语言就显得略有鸡肋。在20实际80年代,计算机界为了解决软件危机提出了面向对象(OOP)思想的变成模式,于是支持OOP的编程语言也应运而生。
1982年Bjarne Stroustrup博士在C语言的基础上引入并且扩充了面向对象的概念,并且命名为C++,因此C++ 是在C语言基础上诞生的,它既可以支持面向过程编程也可以支持面向化程序设计。
C++ 发展至今和C语言一样已经拥有很多各版本,并在不停的升级中,目前最为常用时C++ 98(引入STL,以模板方式重新编写标准库)和C++ 11(增加了很多特性,例如范围for,auto关键字),目前C++已经应用于互联网各个方向,例如大型系统开发,游戏开发,网络工具,嵌入式,数字图像处理……
如何学习
多看书,目前市面上有很多C++ 优秀书籍,有些甚至成为了C++ 工程师心中的标杆(《Effective C++》)。
多记录,每天的学习笔记,每周的学习总结,遇到的问题,这些都要多多记录,方便之后再次遇到相同的问题可以直接拿出来复习。
思维导图,思维导图是学习中必不可少的,可以帮我们理清学习路线,学习思路,方便复习。
多敲代码,语言都是如此多用才是巩固的基础,夺取在线OJ网上练习,或者自己敲几个小项目练练手都是练习的好思路。
命名空间
在一门高级语言中,变量是大量存在的,那么难免在定义变量的时候就会重名,尤其在一个项目或工程中有多个工程师的时候就跟容易与他人定义相同变量名的变量,重名问题就会更加明显,在C语言中我们没有一个有效的办法来解决这个问题,因此C语言在大型项目方面会很吃力,因此在C++中加入了命名空间的语法,不同的程序员之间使用不同的命名空间,在不同的命名空间中允许变量重名,由此在项目合作中十分有用。
命名空间的定义
1 | #include <iostream> |
每一个命名空间都是一个作用域,空间中的内容都局限于该空间中,而我们要使用某空间中的某一变量或函数时指定命名空间即可找到指定的内容。
命名空间支持嵌套,如果一个工程中同时存在多个相同名称的命名空间,则最后会合成到一个命名空间中。
命名空间的使用
在使用命名空间时要加上作用域限定符::
进行作用域的限定。我们右三种使用命名空间的方法。
加命名空间加作用域限定符:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16int main()
{
//三种使用命名空间的方法
//::为作用域限定符
//分别打印三个命名空间中的a;
std::cout << "N1::a = " << N1::a << std::endl;
std::cout << "N2::a = " << N2::a << std::endl;
std::cout << "N2::N3::a = " << N2::N3::a << std::endl;
}
[misaki@localhost 第一章-C++入门]$ ./namespace
N1::a = 1
N2::a = 2
N2::N3::a = 3
我们在进行输出的时候用到了cout
函数以及endl
换行函数,由于这两个函数都在std
标准命名空间中,因此要想使用这两个函数也要用作用域限定符进行限定。
使用using将命名空间中成员引入:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15using N1::a;
using N2::b;
int main()
{
//三种使用命名空间的方法
//::为作用域限定符
//分别打印三个命名空间中的a;
std::cout << "N1::a = " << a << std::endl;
std::cout << "N2::b = " << b << std::endl;
}
[misaki@localhost 第一章-C++入门]$ ./namespace
N1::a = 1
N2::b = 2
使用命名空间名称引入:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15using namespace N1;
int main()
{
//三种使用命名空间的方法
//::为作用域限定符
//分别打印三个命名空间中的a;
std::cout << "N1::a = " << a << std::endl;
std::cout << "N1::b = " << b << std::endl;
}
[misaki@localhost 第一章-C++入门]$ ./namespace
N1::a = 1
N1::b = 1
缺省参数(默认参数)
我们在C语言中书写函数时如果我们为一个函数设置了参数则在调用时必须对参数进行传入否则就会调用失败,但有的时候会出现大量的相同的冗余的参数,我嫩不得不手动将参数一一传入,但是C++中得益于缺省参数的语法是的我们可以在函数中提前设置默认的参数,如果有新参数传入则使用新的参数否则使用默认参数。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22#include <iostream>
using namespace std;
int func1(int a = 10, int b = 20, int c = 30)
{
return a + b + c;
}
int func2(int a, int b = 20, int c = 30)
{
return a + b + c;
}
int main()
{
cout << func1() << endl;
cout << func1(1, 2, 3) << endl;
cout << func2(1) << endl;
}
[misaki@localhost 第一章-C++入门]$ ./缺省参数
60
6
51
在设置默认参数时一定要注意以下几点:
1、为了不产生歧义我们只能从最后一个参数向前开始设置,并且中间不能跳过某个参数要保持默认参数连续。
2、默认值只能是常量或是全局变量。
3、缺省参数不能在函数的定义和声明中同时出现。
函数重载
在C语言中我们要求函数名不能重名,因此往往我们需要功能相同的函数但向外提供不同的接口时就需要通过自己改变函数名的方式来进行区别。但在C++中我们有了函数重载,我们可以通过不同的参数对名字相同的函数进行修饰,使得函数的功能更加强大更加方便。
使用
1 | #include <iostream> |
在C++如果定义同名函数我们必须保证参数列表不同,即参数的个数或参数类型或参数顺序不同才能完成重载,并且返回值不同不会进行函数的重载。
名字修饰
在C++中我们之所以可以使函数重名是因为在编译过程中编译器根据函数的参数列表对我们的函数名进行了处理,使得其在最后得以唯一化,至于其名字修饰规则较为复杂,不同的编译器在修饰中的处理都不尽相同。我们可以利用反汇编看一下gcc的处理规则。1
2
3
4
5
6
7//三个Add()函数在编译中的修饰结果:
int Add(int, int):
000000000040089d <_Z3Addii>
double Add(double, double):
00000000004008d1 <_Z3Adddd>
float Add(float, float):
000000000040091d <_Z3Addff>
从上面的例子中我们大致可以摸索出gcc函数修饰的规则,它会根据参数列表对函数名进行唯一化修饰。无论如何在编译中到最后一步链接之前我们的目标文件中一定不会出现重名的函数,不然在连接时就会产生歧义。
extern “C”
C++ 支持向前兼容,就是说我们可以在C++ 中无缝调用C语言的代码,那么我们可以在C语言中调用C++ 的代码么?答案是肯定的。我们在C++ 的函数中只需要加上extern "C"
就可以做到让编译器根据C语言的编译规则来进行编译,这样我们就可以在C语言中调用C++的代码。1
2
3
4
5
6
7
8
9
10
11
12
13
14#include <iostream>
using namespace std;
extern "C" int Add(int a, int b)
{
return a + b;
}
int main()
{
cout << Add(1, 2) << endl;
}
[misaki@localhost 第一章-C++入门]$ ./externC
3
总结
C++ 支持重载而C语言不支持重载的原因是什么呢?C++支持函数名修饰,而C语言不支持。其实const
也可以进行重载,但是要看情况,const
只能支持指针和引用的重载。因为指针或引用指向不可更改的数据内容也是可以在编译过程中进行函数修饰的。
引用
引用是C++中一种新的类似于指针的语法,但是我们在使用指针指向另外一个变量的时候会创建一个新的指针变量,这个变量会存储指向的变量的地址,但是我们在使用引用的时候并不会分配新的内存空间。所谓引用不过是给某一个变量起了一个别名。
使用
1 | #include <iostream> |
以上就是定义了一个引用b
让它成为a
的别名,之后我们就可以用b
代替a
。
特性
在使用引用的过程中有以下规则:
1、引用在定义的时候必须初始化,不像我们在定义指针的时候如果一开始不使用可以置空,但是引用相当于是别名因此不可以不进行初始化。
2、一个变量可以有多个引用,就像是一个人可以有多个别名一样。
3、一个引用一旦引用一个实体则不能再引用其他实体。一个引用一旦已经变成了某个实体的别名则它不可以再成为其他变量的别名了。
常引用
常引用和常量指针一样是指向不可更改的数据的类型。被它所指向的数据不可进行更改。1
2
3
4
5
6
7
8
9
10
11#include <iostream>
using namespace std;
int main()
{
const int& c = 10;
cout << "a = " << c << endl;
}
[misaki@localhost 第一章-C++入门]$ ./Ref
a = 10
使用场景
做参数
引用和指针一样在传参的使用上有独特的优势,他和指针一样可以将参数本身传入而不是传入副本,因此我们在函数内部可以进行函数参数值的更改。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17#include <iostream>
using namespace std;
void Add(int a, int b, int& result)
{
result = a + b;
}
int main()
{
int result;
Add(1, 2, result);
cout << "1 + 2 = " << result << endl;
}
[misaki@localhost 第一章-C++入门]$ ./Ref
1 + 2 = 3
这样我们就可以将传入参数的值在函数内进行更改,使用比指针更为简单。
做返回值
就像我们如果一个函数的返回值是指针一样,我们如果用引用作为函数的返回值我们就要保证引用的实体在函数声明周期后依然存在。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17#include <iostream>
using namespace std;
int& Add(int a, int b)
{
int result = a + b;
return result;
}
int main()
{
int& result = Add(1, 2);
Add(2, 3);
cout << "result = "<< result << endl;
}
[misaki@localhost 第一章-C++入门]$ ./Ref
result = 5
以上这个result
的值为什么变成了5呢?我们在用引用的时候实际上是给一块内存地址取了别名,我们让函数返回了在函数结束后已经释放的空间的别名。在函数结束后空间中的数据并不会立刻删除,会保留直到下次使用进行覆盖,而我们紧接着再次调用函数,由于栈帧相同使相同的内存空间的数据进行更改,因此我们用别名得到它的数据也会更改。那么如果我们在之后执行一个其他的函数呢?1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20#include <iostream>
using namespace std;
int& Add(int a, int b)
{
int result = a + b;
return result;
}
int main()
{
int& result = Add(1, 2);
Add(2, 3);
cout << "Ref"<< endl;
cout << "result = "<< result << endl;
}
[misaki@localhost 第一章-C++入门]$ ./Ref
Ref
result = 32635
可以看到数据已经变成了完全无关的数据,因此我们如果用引用作为返回值就一定要保证我们引用所指内存空间在函数结束后并不会释放。
指针和引用
1. 引用在定义时必须初始化,指针没有要求。
2. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实
体。
3. 没有NULL引用,但有NULL指针。
4. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4
个字节)。
5. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小。
6. 有多级指针,但是没有多级引用。
7. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理。
8. 引用比 指针使用起来 更为方便。
inline内联函数
在C语言中我们可以用宏来定义常量,定义函数,宏只是单纯的文本替换,因此使用时缺点很多,例如其不支持调试,可读性差,不容易控制,因此在C++中诞生了内联函数,依旧可以帮助我们达到和宏一样的功能减少函数调用生成栈帧的开销。
使用
1 | #include <iostream> |
因此我们在C++中当我们需要写一些小型的函数时为了减少调用时形成栈帧的开销以减少时间就可以加上inline
关键字
注意
1、内联函数不能声明和定义分离 他们必须在通过一个文件中,因为内联函数在调用处会被展开是不会有内存地址的,如果分离是无法进行链接的。
2、就算我们加上inline
关键字到底是否会展开是由编译器决定的,我们仅仅是提个建议。
3、在debug
模式中编译器不会进行代码优化因此默认不会将inline
进行展开。
C++11新增语法
auto关键字
auto
是C++11最新的关键字,可以自动检测类型,因此在某些类型名很长情况下可以起到简化代码的作用。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15#include <iostream>
using namespace std;
int main()
{
int a = 10;
auto b = a;
auto c = 'a';
cout << b << endl;
cout << c << endl;
}
[misaki@localhost 第一章-C++入门]$ ./C++11
10
a
但是使用auto
要注意以下几点:
1、auto
在生命变量时一定要初始化。
2、auto不能作为函数参数或者数组类型,因为无法计算其大小。
3、auto*
与auto
无异,但是在定义引用时要加上&
。
范围for
这个语法十分类似JAVA中的范围for,也是为了简便代码而存在的。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18#include <iostream>
using namespace std;
int main()
{
int arr[5] = {1, 2, 3, 4, 5};
for(auto e : arr)
{
cout << e << endl;
}
}
[misaki@localhost 第一章-C++入门]$ ./C++11
1
2
3
4
5
范围for可以和auto共同使用,这样会更为方便,但是要注意范围for不能遍历传入函数中的数组,因为其实际上穿得是指针。
以上这两种语法我们称之为语法糖,就是可以简化代码的语法,但是这种语法会让代码可读性变差。
nullptr
在C++中有一个新的可以标记空指针的关键字,之前我们使用的NULL
可以完全被他代替,并且我们之前使用的NULL
为一个宏,值为0,未标记类型,因此我们在进行函数传参时,会将我们传入的空指针的值当作int
处理,但是nullptr
的值为0类型为int*
,加入了值类型上的限定,因此在传参和重载中不会出现问题。