文章目录
- 第一章 什么是C++
- 第二章 C++类型增强
- 第三章 封装 (Encapsulation)
- 第五章 类的扩展(Class' Extension)
- 第六章 友元
- 第七章 运算符重载Operator Overload
第一章 什么是C++
1.1 C++之父
1979 年,美国 AT&T 公司贝尔实验室的 Bjarne Stroustrup 博士在 C语言的基础上引 入并扩充了面向对象的概念,发明了一种新的程序语言。为了表达该语言与C语言的渊源 关系,它被命名为C++。而Bjarne Stroustrup(本贾尼·斯特劳斯特卢普)博士被尊称为C++ 语言之父。
1.2 历史背景
C语言作为结构化和模块化的语言,在处理较小规模的程序时,比较得心应手。但是 当问题比较复杂,程序的规模较大的时,需要高度的抽象和建模时,C语言显得力不从心。
1.3 应 “运” 而生,运为何
说这个之前,什么是面向过程编程,什么是面向对象编程,这两个东西呢。只是一种思想而已,就像二级指针和一级指针,只是对格式化内存空间不同逻辑思想而已,简单概括以下就是如下区别:
- 面向过程:编写函数
- 面向对象:编写类
为了解决软件危机,20 世纪 80 年代,计算机界提出了 OOP(Object Oriented Programming)思想,这需要设计出支持面向对象的程序设计语言。Smalltalk 就是当时问 世的一种面向对象的语言。而在实践中,人们发现C语言是如此深入人心,使用如此之广 泛,以至于最好的办法,不是发明一种新的语言去取代它,而是在原有的基础上发展它。 在这种情况下 C++应运而生,在C的基础之上添加面向对象的功能,最初这门语言并 不叫 C++而是 Cwith class (带类的C)。
1.4 发展计事
1.4.1 现代 C++
- 1979 April :Work on C withClasses began
- 1979 October:First C with Classes(Cpre)running
- 1984: C++ named
- 1985 October: Cfront Release 1.0 (firstcommercialrelease) The C++ Programming Language
- 1987 December: First GNU C++ release(1.13)
- 1989:The Annotated C++ Reference Manual; ANSI C++ committee (J16) founded (Washington,DC)
- 1994 string:(templatized by character type) (San Diego, California); the STL accepted(SanDiego,CAandWaterloo,Canada)
- 1998:ISO C++ standard ratified
- 2003:Technical Corrigendum; workon C++0x started
1.5 语言地位
1.6 应用领域
效率重要,且同时需要好的抽象机制的领域。几乎涵盖了,底层的操作系统层,主流 服务器层和主流开发框架层。
1.6.1 系统层软件开发
C++的语言本身的高效和面向对象,使其成为系统层开发的不二之选。比如我们现在用 的window 桌面,GNOME 桌面系统,KDE 桌面系统。
1.6.2 服务器程序开发
面向对像,具有较强的抽像和建模能力。使其在电信,金融,电商,通信,媒体,交 换路由等方面中不可或缺。
1.6.3 游戏网络分布式云计算
1.6.4 丰富的库类
MFC/QT/ACE/Boost/Cocos/CrossApp/Unreal
1.7 开发环境
1.7.1 QT/VS
VS更为强大,QT更为小清新
第二章 C++类型增强
曾有人戏谑的说,C++作为一种面向对象的语言,名字起的不好,为什么呢?用 c 的 语法来看,++ 操作符是 post++,即后++。 post++,即意味着,对C语言的扩展和兼容。扩展即面向对象的扩充,兼容能否作到 百分之百呢?
2.1 Type enhance 类型增强
2.1.1 更严格的类型检查
C语言中 const * ->non-const* / void* ->sometype * / type *-> type * 均是可以的, ->的方向代表转化方向。
最直接的证明就是,c++ 里的const就是一个名副其实的const了。
2.1.2 bool类型
C 语言中没有逻辑类型,C中的逻辑的真假,用 0 表示逻辑真,而非 0 来表示逻辑的假。
而 C++中有了具体的类型,即 bool 类型,有两个值可供选择,true 和 false。但其本 质,仍是一个 char类型的变量,可被 0和非 0的数据赋值。
bool类型的值,除了true 和false以后,还可以被其它类型赋值的。当用sizeof来求 bool类型的大小的时候,发现其大小是1,即一个字节,也就是 char类型的大小。
2.1.3 真正的枚举
C语言中枚举元素类型本质就是整型,枚举变量可以用任意整型赋值。而C++中枚举 变量,只能用被枚举出来的元素赋值。
2.1.4 可被赋值的表达式
C语言中表达式通常不能作为左值的,即不可被赋值, C++中某些表达式是可以赋值的。
2.1.5 nullptr (C++11)
为了避免引起调用或语义上的混淆,C++11,此入了nullptr 用于区分,NULL 和0
2.2 Input/Output 标准输入与输出
cin 和 cout 是 C++的标准输入和输出流对象。他们在头文件 iostream 中定义,其意 义作用类似于 C语言中的 scanf和printf。
scanf和printf 的本质是函数, 而cin和cout是类对象。
#include <iostream>
using namespace std;
int main(int argc, char** argv)
{
int a, b, c;
cin >> a >> b >> c;// 等价于cin>> a; >> b; cin >> c;
cout << a << endl;
cout << b << endl;
cout << c << endl;
system("pause");
return 0;
}
但是cin与scanf一样,都是不安全的:
int main(int argc, char** argv)
{
char buf[10];
cin >> buf;
cout << buf << endl;
system("pause");
return 0;
}
虽然是buf[10], 但是输入大于10个字符的时候,还是大概率行得通。
我们可以使用getline来弥补这一行为:
int main(int argc, char** argv)
{
char buf[10];
cin.getline(buf, 10);
cout << buf << endl;
system("pause");
return 0;
}
输出如下:
但是,最保险的,还是使用string类型,只是c++比起c新添加的类型,在c++中会取代字符数组:
int main(int argc, char** argv)
{
string buf;
cin >> buf;
cout << buf << endl;
system("pause");
return 0;
}
但是,有时候也是不能取代的。这个string到底有多大呢?不妨看一下:
int main(int argc, char** argv)
{
string buf;
cin >> buf;
cout << buf.max_size() << endl;
system("pause");
return 0;
}
输出是2147483647,也就是有符号int的最大值,超过这个值就是不安全的。所以,别超过这个值。
2.2.1 cout的格式化输出:
既然cout是为了取代printf的,那么总得拿出点实力来说服我们他确实比printf方便。就拿格式化输出来说,用法如下:
int main(int argc, char** argv)
{
int a = 30;
cout << a << endl;
cout << hex << a << endl; //16进制
cout << oct << a << endl;//8进制
cout << a << endl; //在上一句中已经将默认输出设置为8进制了
cout << dec << a << endl; //十进制输出
system("pause");
return 0;
}
2.2.2 cout的预宽,左右对齐
设置预宽,需要包含头文件 iomanip,然后使用set()来设置。
int main(int argc, char** argv)
{
float ft = 1.234;
cout << setw(10) << ft << setw(10) << endl;
system("pause");
return 0;
}
左右对齐则需要用到setiosflags()来设置:
int main(int argc, char** argv)
{
float ft = 1.234;
cout << setw(10) << setiosflags(ios::left) << ft << endl;
cout << setw(10) << setiosflags(ios::right) << ft << endl;
system("pause");
return 0;
}
还有一个问题就是填充,现在默认是填充空格。我们能不能用其他字符来填充呢?可以使用setfill()来设置填充比如我们要输出时间12:03:01呢?可以通过以下来设置格式:
int main(int argc, char** argv)
{
int a = 12, b = 3, c = 1;
cout << setfill('0') << setw(2) << a << ":" << setw(2) << b << ":" << setw(2) << c << endl;
system("pause");
return 0;
}
以下是一个练习刷频时间的小练习:
void ShowTime()
{
system("cls");
time_t lt = time(nullptr);
time(<);
struct tm time_now;
localtime_s(&time_now, <);
int hour, min, sec;
hour = (&time_now)->tm_hour;
min = (&time_now)->tm_min;
sec = (&time_now)->tm_sec;
cout << setfill('0') << setw(2) << hour << ":" << setw(2) << min << ":"
<< setw(2) << sec << endl;
}
int main(int argc, char** argv)
{
while (1)
{
ShowTime();
Sleep(1000);
}
system("pause");
return 0;
}
2.2.3 设置浮点数精度
还有一个点需要讲,就是设置浮点数精度。我们可以使用setprecision()来设置。
int main(int argc, char** argv)
{
float ft = 1.23456;
cout << setprecision(2) << ft << endl;
system("pause");
return 0;
}
这个时候,setprecision(2)代表有效数字为2;
cout << setprecision(2) << setiosflags(ios::fixed) << ft << endl;
这个时候,则表示小数位数为2位。
2.3 函数重载
函数重载会出现重名的函数,重名的函数会根据语境来决定声明。
运算符重载也是函数重载
重载的特点:
- 函数的函数名相同
- 函数的参数,类型,个数不同,皆可以构成重载。
- 函数返回值的类型不能作为函数重载的标志。
int AbsNum(int a)
{
return a > 0 ? a : -a;
}
float AbsNum(float a)
{
return a > 0 ? a : -a;
}
int main(int argc, char** argv)
{
int a = -5;
float b = -5.12;
cout << AbsNum(a) << endl;
cout << AbsNum(b) << endl;
system("pause");
return 0;
}
注意:C++允许 int 到 long 和 double, double 到 int 和 float, int 到 short 和 char 等隐式转换,遇到这种情形,则会引起二义性。
2.3.1 重载底层实现
C++利用Name Mangling(命名倾轴)技术,来改变函数名,区分参数不同的函数名。实现原理:vcifld 表示void int float long double 及其引用。在底层实现时,函数名在编译器层面上已经不一样了。
void Foo(double a) // Foo_d
{
cout << "double" << endl;
}
void Foo(long a) //Foo_l
{
cout << "long" << endl;
}
2.3.2 extern “C”
C/C++的编译都是以文件为单位进行编译的。 Name Mangling(命名倾轧) 依据函数声明来进行倾轧的。若声明被倾轧,则调用为倾轧版本,若声明为非倾轧版本,则调用也为非倾轧版本。 C++ 默认所有函数倾轧。若有特殊函数不参与倾轧,则需要使用 extercn “C” 来进行 声明。
2.4 Op Overload 运算符重载
前面用到的运算符<<,本身在C语言中,是位操作中的左移运算符。现在又用作流插入运算符,这种一个运算符多种用处的现像叫作运算符重载。在C语言中本身就有重载的现像,比如 & 既表示取地址,又表示位操作中的与运算。 *既表示;解引用,又表示乘法运算符。只不过此类功能,C语言并没有对开发者开放这类功能。 C++提供了运算符重载机制。可以为自定义数据类型重载运算符。实现构造数据类型也可以像基本数据类型一样的运算特性。比如,我们可以轻松的实现基本数据类型 3+5的运算,确实不能实现两个结构体类型变量相加的运算。
记得之前写的·贪吃蛇游戏的时候,我们需要补比较蛇头与食物的位置是否一样时,我们不能直接在比较 snakeHead.pos == food.pos, 而是分别比较了 x 与 y 的值,这就很不方便:
int IfEatFood(Snake* sh, Food* fd)
{
if (sh->pos_.x_ == fd->pos_.x_ && sh->pos_.y_ == fd->pos_.y_)
return 1;
return 0;
}
这样就很繁琐,C++ 对用户开放了这一系列功能:
typedef struct _pos {
int x_;
int y_;
}Pos;
bool operator ==(Pos one, Pos another)
{
if (one.x_ == another.x_ && one.y_ == another.y_)
{
return true;
}
return false;
}
//运算符重载
int main(int argc, char** argv)
{
Pos ps = {
1,2 };
Pos fdPs = {
1,2 };
if (ps == fdPs) //本质就是operator ==(ps, fdPs)
cout << "equl" << endl;
else
cout << "not equl" << endl;
system("pause");
return 0;
}
2.5 默认参数(Default Arg)
通常情况下,函数在调用时,形参从实参那里取得值。C++给出了可以不用从实参取 值的方法,给形参设置默认值,称为默认参数。默认值,则是一种最为常见的情况,是对真实生活的模似,生活中很难找出没有默认 的东西。(例如,**杀人偿命,欠债还钱 **)故,C++引入默认参数,也是为了方便编程。
默认规则:
- 从右往左,不能跳跃
void Foo(int a=1, int b, int c = 5)
{
cout << "a = " << a << ", b = " << b << ", c = " << c << endl;
}
这样设计坚决不行的,设置默认值必须从右往左且不能跳跃
- 实参个数 + 默认个数 >= 形参个数
void Foo(int a, int b = 0, int c = 0)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl;
}
int main(int argc, char** argv)
{
Foo(5,4); // 2 + 2 = 4
Foo(1); // 1 + 2 = 3
Foo(); // 0 + 2 = 2 < 3 编译不通过
}
- 声明和实现不在一起的时候,默认参数出现在声明中(可能由于编译器不同而不同)
- 默认参数可以是一个常量,全局变量,亦或是一个函数。
2.5.1 规则冲突
重载与默认参数的冲突。这是一个无解的冲突。
- 当实现同一个功能的时候,既可以使用默认参数,也可以使用重载,但是推荐使用默认参数。
void Foo(int a, int b = 0, int c = 0)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl;
}
void Foo()
{
cout << " NULL " << endl;
}
2.6 & 引用(Reference)
2.6.1 定义
变量名,本身是一段内存的引用,即别名(alias)此处引入的引用,是为已知变量起的一个别名。
声明如下:
int main(int argc, char** argv)
{
int a = 0;
int& ra = a;
return 0;
}
此处的 ra 与 a 是的等价关系。此处 ra 是一种声明关系,不开辟额外的空空间。且必须初始化,不能单独存在。与被别名的变量拥有相同的数据类型。
引用关系一旦确定,便不可变更:
int main(int argc, char** argv)
{
int a = 0;
int b = 1;
int& ra = a;
cout << ra << endl;
ra = b;
cout << ra << endl;
return 0;
}
输出如下:
可能会误以为引用已经变更到 b 身上了,其实并没有,这里等价于:
int a = 0, b = 1;
int & ra = a;
ra = b // a = b;
cout << ra << endl; // cout << a << endl;
所以是直接更改了a的值,可以通过取地址来证明:
我们可以对一个变量建立多个引用,此时所有的引用都是一种平行关系。如下:
int main(int argc, char** argv)
{
int a = 0;
int& ra_1 = a;
int& ra_2 = a;
int& ra_3 = ra_2;
return 0;
}
2.6.2 引用的应用
C++很少使用独立变量的引用,如果使用某一个变量,就直接使用它的原名,没有必要使用他的别名。
引用的真正目的,就是取代指针传参。C++引入引用后,可以用引用解决的问题。避免用指针来解决。
还记得怎么交换两个int吗?
void MySwape(int* pa, int* pb)
{
int t = *pa;
*pa = *pb;
*pb = t;
}
//引用的应用
int main(int argc, char** argv)
{
int a = 0, b = 5;
cout << a << " " << endl;
cout << b << " " << endl;
MySwape(&a, &b);
cout << a << " " << endl;
cout << b << " " << endl;
return 0;
}
但是在这里,引用可以完全取代指针,这也正是他的作用所在:
void MySwape(int& ra, int& rb)
{
int t = ra;
ra = rb;
rb = t;
}
//引用的应用
int main(int argc, char** argv)
{
int a = 0, b = 5;
cout << a << " " << endl;
cout << b << " " << endl;
MySwape(a, b);
cout << a << " " << endl;
cout << b << " " << endl;
return 0;
}
效果一模一样。传引用等价于传作用域。把一个变量以引用的方式传到另外一个作用域。等价于扩展了该变量的作用域。
2.6.3 引用的提高
2.6.3.1指针的引用-有, 引用的指针-无
引用的本质是指针,C++对裸露的内存地址(指针)作了一次包装。又取得了指针的优良特性,避免使用裸露的地址。所以再对引用取地址,建立引用的指针没有意义
指针的引用如下:
void MySwape(const char*& p, const char* & q)
{
const char* t = p;
p = q;
q = t;
}
//指针的引用
int main(int argc, char ** argv)
{
const char* p = "China";
const char* q = "Canada";
MySwape(p, q);
cout << "p = " << p << endl;
cout << "q = " << q << endl;
system("pause");
return 0;
}
这告诉我们,原来需要用二级指针解决的事情,现在只需要在平级内解决。这是很棒的东西,不用再指来指去。
对于引用的指针类型,C++避免了对引用再次开放。
const char*& rp = p;
const char&* prp = &re;
这样是编译不过去的。
2.6.3.2 指针的指针-有,引用的引用-无
指针的指针,即二级指针。C++为了避免C语言设计指针的"失误",避免了引用的引用这种情况。由此,也避免了引用的引用的引用的…的引用的情况,这种可穷递归的设计本身就是有问题的。
指针的指针如下:
int main(int argc, char** argv)
{
int a = 5;
int* p = &a;
int** pp = &p;
int*** ppp = &pp;
system("pause");
return 0;
}
这里所讨论的引用的引用,并非之前讨论的对引用再次引用,如下:
int a = 5;
int& ra = a;
int& raa = ra;
int& raaa = raa;
这种情况是完全允许的,因为ra,raa,raaa都是a的别名,他们是平级关系。我所说的是下面这个情况:
int a = 5;
int& ra = a;
int&& raa = ra;
这样就叫作对引用再次引用,也就是二级引用,和二级指针的思想一样。但是这种是编译不过的,在C++里不存在这种逻辑。如果放开这个逻辑的话,接下来就会出现三级引用, 四级引用, 五级引用。。。这样就刹不住车了,不也就成为了另外的一个指针吗?平级就能解决的问题,为什么又要去跨层解决?
2.6.3.3 指针的数组-有,引用的数组-无
看下面的几个情况:
int a, b, c;
int *p[] = {
&a, &b, &c};//指针数组
int & q[] = {
a, b, c};//引用数组
其中,第二个是铁定编译不过去的。
可以通过反证法来证明。
第一个例子,指针数组的首元素是 int * 型,所以首元素的地址就是 int ** ·型的。
第二个例子, 引用数组的首元素是 int &型,所以首元素的地址就应该是 int & * 的,我们知道,引用的指针是不存在的,所以第二个是编译不过去的。
2.6.3.4 数组的引用
数组是一种构造类型,那么数组可以被引用吗?
答案是可以的。
数组名,本质是一个常量指针,对于指针是可以取引用的,所以数组的引用是存在的, 如下示例。
数组名代表了首元素地址,也是整个数组的唯一标识符。那么就从这个地方出发去研究, 先研究当数组名代表首元素地址时的情况:
int main(int argc, char** argv)
{
int arr[5] = {
0,1,2,3,4 };
int* const& parr = arr;
for (int i = 0; i < 5; i++)
{
cout << parr[i] << endl;
}
system("pause");
return 0;
}
其次是数组名当作一个整体,代表整个数组的情况。int arr[5] 实际上是 int[5] 类型的,可得:
- int[5] &parr = arr, 换写成 int &arr[5],由于我们知道,引用的数组是不存在的,那么又可得:
- int (&arr)[10] = arr
2.7 常引用
2.7.1 Const Semantic
C++中 const 定义的变量称为常变量。const 修饰的变量,有着变量的形式,常量的作用,用作常量,常用于取代#define 定义的宏常量。
2.7.1.2 常引用的特性
const的本意,即不可修改。所以,const对象,只能声明为const 引用,使其语义保持一致性。 non-const 对象,既可以声明为 const 引用,也可以声明为 no-const 引用。 声明为const引用,则不可以通过const 引用修改数据。
int main(int argc, char** argv)
{
const int a = 100;
const int & ra = a;
int b = 200;
const int& rb = b;// 只具备读的功能。
int c = rb + ra;
system("pause");
return 0;
}
2.7.1.3 临时对象的常引用
临时对象,通常理解为不可以取地址的对象,比如函数的返回值,即Cpu中计算产生的中间变量通常称为右值。常见临时值有常量,表达式等。
以下办法是行不通的:
int& a = 55;
但是,我们可以用常引用来引用它:
const int& a = 55;
也可以对表达式进行引用:
int b = 5, c = 3;
const int& e = b + c;
再来研究函数的返回值:
int ReturnA()
{
int a = 100;
return a;
}
int main(int argc, char** argv)
{
int f = ReturnA();
return 0;
}
我们吧ReturnA的值返回给一个整型 f 当然是可以的,但是返回给一个整型引用就会出问题,正确的做法依旧是通过const来解决:
int ReturnA()
{
int a = 100;
return a;
}
int main(int argc, char** argv)
{
const int& f = ReturnA();
return 0;
}
可以理解为,ReturnA在返回时产生了一个中间变量,这个中间变量时在CPU里面的,所以这时候的引用是引用了在CPU中的一个临时变量。
还有一种情况,当引用类型与被引用类型不同的时候,也可以用const来解决:
double p = 3.14;
const int& rp = p;
cout << rp << endl;
此时输出为 3
造成这个的原理是什么呢?其实原理还是中间变量。可以来看一下:
double a = 3.14;
const int& ra = a;
cout << "a = " << a << endl;
cout << "ra = " << ra << endl;
a = 4.14;
cout << "a = " << a << endl;
cout << "ra = " << ra << endl;
输出如下:
发现,ra尽管是对a的引用,但是当a发生改变时,ra的值并没有改变。其实本质就是发生了如下的变化:
int & t = a;
const int & ra = t;
常引用申请了新内存以存放初始化的值,并以const修饰,这样就可以实现对引用对象的类型转换,不过这样也等于引用了新的对象,跟最初的引用对象就没关系了。
2.8 引用的本质
之前稍微提到过一点,引用的本质就是指针。但是我们要如何其去验证这一观点呢,原来那套直接 sizeof 的办法肯定行不通的了。可以如下去严重:
struct TypeC {
char c;
};
struct TypeP {
char* pc;
};
struct TypeR {
char& rc;
};
int main(int argc, char** argv)
{
cout << "sizeof(TypeC) = " << sizeof(TypeC) << endl;
cout << "sizeof(TypeP) = " << sizeof(TypeP) << endl;
cout << "sizeof(TypeR) = " << sizeof(TypeR) << endl;
return 0;
}
输出如下:
这里可以大致推敲出来,引用实际上就是一个指针。但是,引用他必须初始化,什么样的数据类型需要初始化呢?常量就必须初始化。所以,引用就是一个常指针,且一经声明则不可以更改。它的结构形式就应该如下:
int * const p
我们在使用引用的时候,会发现引用和指针使用起来不一样。但是我可以很负责的讲,引用脱去面上的包装,在底层实现上和指针的实现是一模一样的。可以写两个Swape,一个用指针实现,一个用引用实现,在编译器里调试,汇编层面会暴露的一览无余。
2.9 new/delete 堆内存操作
C语言中提供了 malloc 和 free 两个系统函数,完成堆内存的申请和释放。而C++则提供了两个关键字 new 和 delete。此两个关键字是为类对象而设计的。
2.9.1 new/new[]
2.9.1.1 单变量
int* pt = (int*)malloc(sizeof(int));
int* p = new int;
以上是C与C++申请堆内存的操作。在C++中可以如下对申请的内存进行初始化:
int* p = new int(10);
cout << *p << endl;
此时输出就为10,但是一般不这样初始化,还是老老实实用平常的办法:
int* pt = (int*)malloc(sizeof(int));
int* p = new int;
*p = 10;
那我们要 new 一个指针的话,就得用二级指针咯:
int** pp = new int*;
*pp = p;
结构体一样可以new:
Stu* ps = new Stu;
以上就是单变量,接下来看看申请一堆空间。
2.9.1.2 申请一或多维数组
new 关键字,后面跟上类型和维度,比如申请一个 10个int类型大小的数组,即 new int[10], 后面也可以跟初始化数据。
float* p = new float[10]{
1.2,1.4,1.6 };
不过我们通常也不会在这里直接初始化。
接下来是指针数组的申请:
char** pp = new char* [11];
for (int i = 0; i < 10; i++)
{
pp[i] = (char*)"China";
}
pp[10] = nullptr;
while (*pp!=nullptr)
{
cout << *pp++<< endl;
}
多维数组的申请:
//二维空间-数组指针
int(*ptr)[5] = new int[3][5];
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 5; j++)
{
ptr[i][j] = i + j;
}
}
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 5; j++)
{
cout << ptr[i][j]<<" "