目录
0 引言
C++是在C语言基础上增加进了面向对象的编程思想,从而拓展升级的一门强大的编程语言。
C语言是结构化和模块化的语言,适合处理较小规模的程序。对于复杂的问题,规模较大的程序,需要高度的抽象和建模时,C语言则不合适。为了解决软件危机, 20世纪80年代, 计算机界提出了OOP(object oriented programming:面向对象)思想,支持面向对象的程序设计语言应运而生。
1982年,Bjarne Stroustrup博士在C语言的基础上引入并扩充了面向对象的概念,发明了一种新的程序语言。为了表达该语言与C语言的渊源关系,命名为C++。因此:C++是基于C语言而产生的,它既可以进行C语言的过程化程序设计,又可以进行以抽象数据类型为特点的基于对象的程序设计,还可以进行面向对象的程序设计。正是因此背景,C++对于C语言的语法是全面兼容的,C语言的一切语法在C++中都适用。
C++既可以进行C语言的过程化程序设计,又可以进行以抽象数据类型为特点的基于对象的程序设计,还可以进行以继承和多态为特点的面向对象的程序设计。C++擅长面向对象程序设计的同时,还可以进行基于过程的程序设计。 C++几乎可以创建任何类型的程序:游戏、设备驱动程序、云、桌面、嵌入式和移动应用等。 甚至用于其他编程语言的库和编译器也使用C++编写。
1 C++关键字(C++98)
相较于C语言的32个关键字,C++总共有63个关键字。
下述是C++关键字一览
asm | do | if | return | try | continue |
auto | double | inline | short | typedef | for |
bool | dynamic_cast | int | signed | typeid | public |
break | else | long | sizeof | typename | throw |
case | enum | mutable | static | union | wchar_t |
catch | explicit | namespace | static_cast | unsigned | default |
char | export | new | struct | using | friend |
class | extern | operator | switch | virtual | register |
const | false | private | template | void | true |
const_cast | float | protected | this | volatile | while |
delete | goto | reinterpret_cast |
2 命名空间
先来看如下C语言程序:
#include <stdio.h>
#include <stdlib.h>
int rand = 10;
int main()
{
printf("%d\n", rand);
return 0;
}
这段代码实不能够被运行通过的,编译器会直接报错: error C2365: “rand”: 重定义;这是由于stdlib.h中有一个叫rand的函数,而又定义了一个全局变量rand,这就会导致重定义的问题,C语言是不能解决这种问题的。
在C/C++中,变量、函数和C++的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。
2.1 命名空间的定义
定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,名字可以按照自己的喜好来取,然后接一对{}即可,{}中即为命名空间的成员。命名空间中可以定义变量、类型、函数,同时命名空间支持嵌套定义。
如:
namespace mj
{
//命名空间中可以定义变量/函数/类型
int r = 10;
int Add(int left, int right)
{
return left + right;
}
struct Node
{
struct Node* next;
int val;
};
namespace xxx
{
int r = 1;
}
}
ps:同一个工程中还允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中。
一个命名空间就定义了一个新的作用域,命名空间中的所有内容都局限在该命名空间中。
2.2 命名空间的使用(授权)
命名空间可以看做是一个封闭的空间,它的权限很高,如果不对其中的成员进行展开(授权)就不能正常使用。
此处就需要引入新的关键字using来帮助我们使用命名空间,这里有三种使用方法:
2.2.1 直接使用
这里需要引入一个新的运算符:域作用限定符::
按照 命名空间名字::命名空间成员 的方式使用。
int main()
{
printf("%d\n", mj::r);
return 0;
}
2.2.2 部分展开(授权)
假如我们只需要使用命名空间中部分成员,那么就可以使用部分展开(授权)的方式。以using 命名空间名字::命名空间成员名的方式。
以上文的mj为例,使用方法如下:
//部分展开(授权)
using mj::Add;
using mj::r;
int main()
{
printf("%d\n",r);
printf("%d\n", Add(1, 2));
return 0;
}
2.2.3 全部展开(授权)
全部展开是通过using关键字直接展开命名空间内所有成员,以using namespace 命名空间名字 的方式展开,如:
using namespace mj;
3 C++的输入&输出
C++是有很多标准库的,就像C语言的输入输出函数包含在stdio.h一样,C++的标准输入输出流是iostream。
#include <iostream>
//std是c++标准库的命名空间
using namespace std;
int main()
{
int i;
double j;
//>>流提取运算符
cin >> i >> j;
//<<流插入运算符
cout << i << ' ';
cout << j << endl;
return 0;
}
说明:
1. 使用cout标准输出对象(控制台)和cin标准输入对象(键盘)时,必须包含< iostream >头文件
以及按命名空间使用方法使用std。
2. cout和cin是全局的流对象,endl是特殊的C++符号,表示换行输出,他们都包含在包含<
iostream >头文件中。
3. <<是流插入运算符,>>是流提取运算符。
4. 使用C++输入输出更方便,不需要像printf/scanf输入输出时那样,需要手动控制格式。
C++的输入输出可以自动识别变量类型。
5. 实际上cout和cin分别是ostream和istream类型的对象,>>和<<也涉及运算符重载等知识,
这些知识我们我们后续才会学习,所以我们这里只是简单学习他们的使用。后面还会更深入的学习IO流用法及原理
注意:早期标准库将所有功能在全局域中实现,声明在.h后缀的头文件中,使用时只需包含对应
头文件即可,后来将其实现在std命名空间下,为了和C头文件区分,也为了正确使用命名空间,
规定C++头文件不带.h;旧编译器(vc 6.0)中还支持<iostream.h>格式,后续编译器已不支持,因
此推荐使用<iostream>+std的方式。
std是C++标准库的命名空间,对于std的展开是由规范的:
1. 在日常练习中,建议直接using namespace std即可,这样就很方便。
2. using namespace std展开,标准库就全部暴露出来了,如果我们定义跟库重名的类型/对
象/函数,就存在冲突问题。该问题在日常练习中很少出现,但是项目开发中代码较多、规模
大,就很容易出现。所以建议在项目开发中使用,像std::cout这样使用时指定命名空间 +
using std::cout展开常用的库对象/类型等方式。
4 缺省参数
4.1 缺省参数概念
C++是支持缺省参数的,这也是C++与C语言的区别之一。
缺省参数是指声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实
参则采用该形参的缺省值,否则使用指定的实参。
如以下程序,输出结果分别是0和10
void Func(int a = 0)
{
cout<<a<<endl;
}
int main()
{
Func(); // 没有传参时,使用参数的默认值
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(1); Func(1,2); Func(1, 2, 3); return 0; }
全缺省参数就是函数中的所有参数都是缺省参数,此时传参规则是从左往右依次传参。没有传参的以默认值处理。
- 半缺省参数
void Func(int a, int b = 10, int c = 20) { cout<<"a = "<<a<<endl; cout<<"b = "<<b<<endl; cout<<"c = "<<c<<endl; }
此处需要特别注意:
1. 半缺省参数必须从右往左依次来给出,不能间隔着给;
2. 缺省参数不能在函数声明和定义中同时出现,这是因为如果生命与定义位置同时出现,恰巧两个位置提供的值不同,那编译器就无法确定到底该用那个缺省值。
3. 缺省值必须是常量或者全局变量
5 函数重载
函数重载是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数或类型或类型顺序)不同,常用来处理实现功能类似数据类型不同的问题。
可以通过以下程序来感受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;
}
运行结果:
注意:如果函数形参个数相同,类型相同,顺序也相同,但是函数返回值类型不同,是不能构成函数重载的。比如以下程序:
int f(double a, int b)
{
cout << "int f(double , int b)" << endl;
return 0;
}
void f(double a, int b)
{
cout << "void f(double , int b)" << endl;
}
假设我们调用f(1.1 , 1 ),此时编译器不知道会调用上述两个函数中的哪一个,会造成歧义。所以这种情况是不能构成函数重载的。
6 引用
6.1 引用概念
引用是C++在C语言基础上的一个升级。引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
就比如说,一个人可以有多个代号。我们可以用名字来代表一个人,也可以用其他的外号来指代一个人。无论是用姓名还是用外号,指代的都是同一个人。也就是说,C++中的引用,就是对于同一个变量有多个名字,所有的名字指向的都是同一个东西。
引用的格式是:类型& 引用变量名(对象名) = 引用实体;
void TestRef()
{
int a = 10;
int& ra = a;//<====定义引用类型
printf("%p\n", &a);
printf("%p\n", &ra);
}
上述程序就是引用的一个简单实例,ra和a是等价的,他们都代表着内存中的同一块空间。注意:引用类型必须和引用实体是同种类型的
6.2 引用特性
引用有三个重要的特性:
- 引用在定义时必须初始化,否则编译器会报错;
- 一个变量可以有多个引用;
- 引用一旦引用一个实体,就不能再引用其他实体。
6.3 引用使用场景
引用一般有两种常用的使用场景:做参数和做返回值。
6.3.1 做参数
引用作为参数使用时,需要在函数参数声明类型。
一般情况下,如果函数需要用到指针的地方,都可以改为使用引用,会比较方便。
以经典的交换函数为例,下面是C语言版本:
void Swap(int* left, int* right)
{
int temp = *left;
*left = *right;
*right = temp;
}
下面是C++版本:
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
由于引用变量就是原变量的别名,因此对于引用变量的修改就等价于对原变量的修改。对于不支持引用的C语言,实现两个变量的交换就需要传地址调用,本质上也是一种传值传参;而C++的方式叫做传引用传参,大大增加了代码的可读性,方便了设计者。
传引用传参的效率是高于传值传参的。于是可以总结出传引用传参的两个优点:
- 提高效率;
- 应对输出型参数时更加方便(形参修改影响实参)
6.3.2 做返回值
我们先来看如下程序:
int Add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int ret = Add(1, 2);
return 0;
}
对于这个程序,函数Add应该返回3,但是这里的返回并不是直接将c返回给ret,因为出函数作用域之后,函数栈帧会被销毁,即c指向的空间会被回首。此时c指向的对象是未知的。因此,编译器是创建一个临时变量,将c的值复制给这个临时变量,再将临时变量的值赋值给ret。这就是传值返回的正确逻辑。
传引用返回代码如下:
int& Add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int ret = Add(1, 2);
return 0;
}
传引用返回是这样的:在原函数类型的基础上,将类型改写为引用类型。如上述代码,此时函数最后返回的并不是c,而是c的别名。
不过这种写法是有问题的,以上述代码为例,函数栈帧销毁后,即使我们得到了c的别名并将其赋值给ret,可以通过ret找到c指代的空间。但是在栈帧销毁以后,c原本指代的空间已经被回收,此时在通过ret去访问就会形成类似野指针的情况。因此这种情况下使用传引用返回是不恰当的,应当使用传值返回。
不过,假如我们可以确保出了函数作用域之后原对象并没有被影响,就可以使用传引用传参来提高效率。我们以顺序表的修改为例:
C语言版:
struct SeqList
{
int a[10];
int size;
};
/*C的接口设计
读取第i个位置的值*/
int SLAT(struct SeqList* ps, int i)
{
assert(i < ps->size);
// ...
return ps->a[i];
}
//修改第i个位置的值
void SLModify(struct SeqList* ps, int i, int x)
{
assert(i < ps->size);
// ...
ps->a[i] = x;
}
int main()
{
struct SeqList s;
s.size = 3;
// ...
SLModify(&s, 0, 10);
SLModify(&s, 1, 20);
SLModify(&s, 2, 30);
cout << SLAT(&s, 0) << endl;
cout << SLAT(&s, 1) << endl;
cout << SLAT(&s, 2) << endl;
return 0;
}
C++版:
int& SLAT(struct SeqList& ps, int i)
{
assert(i < ps.size);
// ...
return (ps.a[i]);
}
int main()
{
struct SeqList s;
s.size = 3;
// ...
SLAT(s, 0) = 10;
SLAT(s, 1) = 20;
SLAT(s, 2) = 30;
cout << SLAT(s, 0) << endl;
cout << SLAT(s, 1) << endl;
cout << SLAT(s, 2) << endl;
return 0;
}
因为对于顺序表,其数组空间是在全局定义的,出了函数作用域之后也不会被修改,于是就可以使用传引用返回的方式,直接对返回对象进行修改,这样就可以省略一个Modify函数的设计。
综上可以得出传引用返回的两个优点:
- 提高效率;
- 可以直接修改返回对象
同时,传引用返回也有一个缺陷,那就是只有出了函数作用域原对象还在(即占用的内存空间未被修改)才可以使用,否则会造成类似野指针一样不安全的情形。
6.4 引用和指针的区别
引用和指针有如下几个不同点:
- 引用概念上定义一个变量的别名,指针存储一个变量地址。
- 引用在定义时必须初始化,指针没有要求
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
- 没有NULL引用,但有NULL指针
- 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
- 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
- 有多级指针,但是没有多级引用
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
- 引用比指针使用起来相对更安全