为了避免一个文件被多次include,有两种方式:

  • 使用宏来防止同一个文件被多次包含
    • 优点:可移植性好
    • 缺点:无法防止宏名重复,难以排错
1
2
3
4
#ifndef _SOMEFILE_H_
#define _SOMEFILE_H_

#endif

  

  • 使用编译器来防止同一个文件被多次包含
    • 优点:可以防止宏名重复,易排错
    • 缺点:可移植性不好,windows支持,其他平台未必
1
#pragma once

指针

参数传递

下列程序是否正确:

1
2
3
4
5
6
7
8
9
10
11
void GetMemory(char *p)
{
p = new char[100];
}
void Test(void)
{
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}

错误,程序崩溃。因为GetMemory 并不能传递动态内存, Test 函数中的 str 一直都是 NULL。strcpy(str, "hello world");将使程序崩溃。

1
2
3
4
5
6
7
8
9
10
11
char* GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char *str = NULL;
str = GetMemory();
printf(str);
}

可能返回乱码。因为GetMemory 返回的是指向“栈内存”的指针,该指针的地址不是 NULL,但其原现的内容已经被清除,新内容不可知。

1
2
3
4
5
6
7
8
9
10
11
void GetMemory2(char **p, int num)
{
*p = new char[num];
}
void Test(void)
{
char *str = NULL;
GetMemory2(&str, 100);
strcpy(str, "hello");
printf(str);
}

能正常运行,但是内存泄漏。

1
2
3
4
5
6
7
8
9
10
11
oid Test(void)
{
char *str = new char[100];
strcpy(str, "hello");
delete[ ] str;
if (str != NULL)
{
strcpy(str,"world");
printf(str);
}
}

篡改动态内存区的内容,后果难以预料,非常危险。因为 delete[ ]str;之后,str成为野指针(需要str = NULL;)if(str != NULL)语句不起作用。

编程

不用C++库函数,自行实现strcpy

1
2
3
4
5
6
7
8
char *strcpy(char *strDest, const char *strSrc);
{
assert((strDest!=NULL) && (strSrc !=NULL));
char *address = strDest;
while( (*strDest++ = * strSrc++) != ‘\0’ )
;
return address ;
}

  如果说面向对象是一种通过间接层来调用函数以换取一种抽象(创建一个接口类),那么泛型编程则是更直接的抽象,它不会因为间接层而损失效率。不同于面向对象的动态期多态,泛型编程是一种静态期多态,通过编译器生成最直接的代码。泛型编程可以将算法与特定类型、结构剥离,尽可能复用代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//通过函数的方法实现输出最大值
//需要针对不同的输入参数写不同的代码
int max(int a, int b)
{
return a > b ? a: b;
}

double max(double a, double b)
{
return a > b ? a: b;
}

#include <iostream>
using namespace std;
int main()
{
//调用函数
cout << max(1, 2) << endl;
cout << max(1.3, 2.4) << endl;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//通过泛型编程的方法实现输出两数的最大值

// 模板函数
//class不是面向对象的class,这个class表示T是个泛型
//T可以表示任何类型,具体代表哪个数据类型取决于编译期代码生成的是什么类型
template<class T>
T max(T a, T b)
{
return a > b ? a:b;
}

#include <iostream>
using namespace std;
int main()
{
//调用函数
//编译期时,编译器自动生成不同类型的方法
cout << max(1, 2) << endl;
cout << max(1.3, 2.4) << endl;
cout << max('a', 'b') << endl;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/*
泛型编程的主要工作不是程序员,而是编译器
编译器会在编译期间根据代码自动生成方法
由于大量工作是编译器操作,可能通用的泛型函数并不能满足我们的需求
所以我们通常还需要“特化”处理
*/


//特化
//如果是字符串的话特殊处理
//其余的输入可以让输入类型不相同,返回值统一为int
template<>
char* max(char* a, char* b)
{
return (strcmp(a, b) > 0 ? (a) : (b));
}
template<class T1, class T2>
int max(T1 a, T2 b)
{
return static_cast<int>(a > b ? a : b);
}

#include <iostream>
using namespace std;
int main()
{
//调用函数
char* s1 = "hello";
char* s2 = "world";
cout << max(s1, s2) << endl;
cout << max(2, 4.5) << endl;
}

  泛型编程是把算法和具体的数据结构分开了,我们不需要考虑类型本身是什么,直接用一套逻辑把所有的类型都涵盖了,如果需要针对某些特殊类型做处理,我们就进行单独的“特化”。这里比较复杂的操作是编译器的推理过程,程序员所做的工作无非是把该定义好的类型通知编译器,让编译器帮助我们做处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/*
泛型编程的思想可以做一些算法优化,比如下面的等差数列求和
*/

// 1+2+3...+100 ==> n*(n+1)/2

//这里用泛型不是用泛型编程可以传递广泛类型的属性,这里直接定义了int
//这里是借用泛型编程的另一个特征:编译期推理,让程序自动生成代码
template<int n>
struct Sum
{
// 递推的思路,求前n个数的和可以表示为前(n-1)个数的和+n
// Sum(n) = Sum(n-1)+n
// 这里声明一个enum的成员,内部的枚举值是N,N的计算方法是递归求和
// 利用泛型编程的自动推理完成编译期计算
enum Value {
N = Sum<n-1>::N+n
};
};
//下面这个就是递推的基准点,n=1的特化
// 如果不设置,那么就会无穷递归
template<>
struct Sum<1>
{
enum Value {N = 1}; // n=1
};

int main()
{
//这里只需要输出结果就行,运算过程中在编译期就完成了
cout << Sum<100>::N << endl; //计算1+2+……+99+100
return 0;
}

  模板编程的难点很大程度上在于对编译器的理解,我们需要直到怎么帮助编译器提供需要生成代码的信息。

设计模式

  一个模式描述了一个不断发生问题及这个问题的解决方案;模式是前人的设计经验上总结出来的对于一些普遍存在的问题提供的通用解决方案,比如单例模式、观察者模式等;

  软件工程中有很多模式,面向对象常见的有23种设计模式,可以分为创建型(单例),结构型(适配器)和行为型(观察者)模型。设计模式不是万能的,它建立在系统变化点上,哪里有变化哪里就可以用。设计模式是为了解耦合,为了扩展,它通常是演变过来的,需要演变才能准确定位。设计模式是一种软件设计方法,不是标准,面目前大部分框架中都包含了大量的设计模式思想。

  

单例模式

  有些时候,我们需要整个程序中有且只有一个实例,比如系统日志、Windows资源管理器窗口,数据库分配主键等操作。单例的实现思路:

  1. Singleton拥有一个私有的构造函数,确保用户无法通过new直接实例它;
  2. 包含一个静态私有成员变量instance与静态公有方法Instance(),让外部通过Instance()来获取实例;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//Singleton.h

class Singleton
{
public:
//只有静态的方法才能访问静态的变量
static const Singleton* getInstance();
static void DoSomething()
{
cout << "Do Something" << endl;
}
// 将构造和析构函数私有化,防止外部访问
private:
Singleton();
~Singleton();

// 使用静态变量帮助解决资源的分配和释放
// 肯定不能使用栈上的对象,不然会销毁
// 如果使用堆上的对象,在getInstance()方法中new出来,那就涉及到资源释放问题
// 析构函数是private的,外部是无法直接释放的
// 静态变量在程序中属于全局区,但声明了private后可见区域只在类内
// 所以可以让这个实例随着程序的产生而产生,随着程序的灭亡而灭亡
static Singleton* This;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//Singleton.cpp

Singleton* Singleton::This = nullptr; //静态变量需要显式声明


const Singleton* Singleton::getInstance()
{
//这里只是简化了,还需要考虑多线程的情况
if (!This)
{
This = new Singleton;
}
return This;
}

Singleton::Singleton()
{
}

Singleton::~Singleton()
{
}
1
2
3
4
5
6
7
8
int main()
{
Singleton::getInstance()->DoSomething();

Singleton::getInstance()->DoSomething();

return 0;
}

  

  上面的单例模式实现,程序开始时全局实例Singleton* Singleton::This为空,只有我们主动调用Singleton::getInstance()->DoSomething()方法时才会产生这个对象的实例。我们也可以采用饿汉的方式,程序启动时就创建实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//Singleton.cpp

Singleton* Singleton::This = new Singleton(); //这里直接new

const Singleton* Singleton::getInstance()
{
//这里只是简化了,还需要考虑多线程的情况
if (!This)
{
This = new Singleton;
}
return This;
}

Singleton::Singleton()
{
}

Singleton::~Singleton()
{
}

  

观察者模式

  在观察者模式中,观察者需要直接订阅目标事件;在目标发出内容改变的事件后,直接接收事件并作出相应;对象通常是一对多关系。常见于各种MVC的框架中,Model的变化通知各种类型的View时几乎都存在这种模式。

  观察者模式的实现思路:把问题的职责解耦合,将Observable和Observer抽象开,分清抽象和实体。

1
2
3
4
5
6
7
8
9
10
11
//观察者类

class Observer
{
public:
Observer() { ; }
virtual ~Observer() { ; }

// 当被观察对象发生变化时,通知被观察者调用这个方法
virtual void Update(void* pArg) = 0;
};

用list列表来存储被观察清单。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
//被观察对象

class Observer;

class Observerable
{
public:
Observerable();
virtual ~Observerable();

// 注册观察者(加入被观察列表)
void Attach(Observer* pOb);
// 反注册观察者
void Detach(Observer* pOb);

int GetObseverCount() const
{
return _Obs.size();
}

void DetachAll() //清理所有订阅者
{
_Obs.clear();
}

virtual void GetSomeNews(string str)
{
SetChange(str);
}
protected:
void SetChange(string news); // 有变化,需要通知

private:
// 通知观察者
void Notify(void* pArg);

private:
bool _bChange;
list<Observer*> _Obs; //观察对象列表
};


// 注册观察者
void Observerable::Attach(Observer* pOb)
{
if (pOb == NULL)
{
return;
}

// 看看当前列表中是否有这个观察者
auto it = _Obs.begin();
for (; it != _Obs.end(); it++)
{
if (*it == pOb)
{
return;
}
}

_Obs.push_back(pOb);
}

// 反注册观察者
void Observerable::Detach(Observer* pOb)
{
if ((pOb == NULL) || (_Obs.empty() == true))
{
return;
}

_Obs.remove(pOb);
}

void Observerable::SetChange(string news)
{
_bChange = true;

Notify( ( (void*)news.c_str() ));
}


void Observerable::Notify(void* pArg)
{
if (_bChange == false)
{
return;
}

// 看看当前列表中是否有这个观察者
auto it = _Obs.begin();
for (; it != _Obs.end(); it++)
{
(*it)->Update(pArg);
}

_bChange = false;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
//main

//被观察对象
class News : public Observerable
{
public:
virtual void GetSomeNews(string str)
{
SetChange("News: " + str);
}
};

//两个观察者
class User1:public Observer
{
public:
virtual void Update(void* pArg)
{
cout << "User1 Got News: " << reinterpret_cast<char*>(pArg) <<endl;
}
};
class User2 :public Observer
{
public:
virtual void Update(void* pArg)
{
cout << "User2 Got News: " << reinterpret_cast<char*>(pArg) <<endl;
}
};

int main()
{
User1 u1;
User2 u2;

News n1;
n1.GetSomeNews("t0"); //此时没人订阅,什么事情都不会发生

//订阅
n1.Attach(&u1);
n1.Attach(&u2);
n1.GetSomeNews("t1");

n1.Detach(&u2);
n1.GetSomeNews("t2");

return 0;
}

  观察者模式帮助我们把职责关系梳理清晰了,我们如果要增加一个新的观察者,直接继承一个Observer类,实现一下消息更新的虚方法即可。

适配器(Adapter)模式

  适配器模式可以理解为一个插口转换器,去不同国家旅游时可以适配不同的插座接口。适配器将类接口转换为客户端期望的另一个接口,让接口更兼容。适配器模式的动机是:如果可以更改接口,则可以重用现有的软件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
//适配器模式
//场景:
// 已经有一个矩形处理类LegacyRectangle,可以画一个矩形
// 此时收到需求,在画矩形之前需要额外输入一些别的东西,原有的LegacyRectangle类不能满足需求
//


//原始的矩形处理类
class LegacyRectangle
{
public:
LegacyRectangle(double x1, double y1, double x2, double y2)
{
_x1 = x1;
_y1 = y1;
_x2 = x2;
_y2 = y2;
}

void LegacyDraw()
{
cout << "LegacyRectangle:: LegacyDraw()" << _x1 << " " << _y1 << " " << _x2 << " " << _y2 << endl;
}

private:
double _x1;
double _y1;
double _x2;
double _y2;
};



// 创建一个抽象类和外部对接,让外部去调用这个接口
class Rectangle
{
public:
virtual void Draw(string str) = 0;
};


//创建内部接口,来实现对原始LegacyRectangle代码的复用
//有两种方式:

// 第一种适配的方式:使用多重继承,把原始的类一并继承
class RectangleAdapter: public Rectangle, public LegacyRectangle
{
public:
RectangleAdapter(double x, double y, double w, double h) :
LegacyRectangle(x, y, x + w, y + h)
{
cout << "RectangleAdapter(int x, int y, int w, int h)" << endl;
}

virtual void Draw(string str)
{
cout << "RectangleAdapter::Draw()" << endl;
LegacyDraw(); //实际使用的还是原始的方法
}
};

// 第二种适配的方式:组合方式的Adapter,把原始的类当作成员变量
// 这种方式更常见,因为可以避免多重继承方式的强耦合性
class RectangleAdapter2 :public Rectangle
{
public:
RectangleAdapter2(double x, double y, double w, double h) :
_lRect(x, y, x + w, y + h)
{
cout << "RectangleAdapter2(int x, int y, int w, int h)" << endl;
}

virtual void Draw(string str)
{
cout << "RectangleAdapter2::Draw()" << endl;
_lRect.LegacyDraw();
}
private:
LegacyRectangle _lRect;
};

int main()
{
double x = 20.0, y = 50.0, w = 300.0, h = 200.0;
//实际创建的是适配器对象,但接口调用用的是Rectangle接口类
RectangleAdapter ra(x, y, w, h);
Rectangle* pR = &ra;
pR->Draw("Testing Adapter");

cout << endl;

RectangleAdapter2 ra2(x, y, w, h);
Rectangle* pR2 = &ra2;
pR2->Draw("Testing2 Adapter");

return 0;
}

  计算机程序的输入流起点和输出流的终点都可以是磁盘文件。C++把每个文件都看成是一个有序的字节序列,每个文件都以文件结束标志结束。

  按照文件中数据的组织形式可以把文件分为:

  1. 文本文件:文件中信息形式为ASCII码文件,每个字符占用一个字节;
  2. 二进制文件:文件中信息的形式与其在内存中的形式相同;

  

文件操作步骤:

  1. 打开文件open;
  2. 检查打开是否成功;
  3. 读或写read\write;
  4. 检查是否读完EOF(end of file);
  5. 使用完文件后关闭文件close;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//把输入的字符信息写入文件

#include <iostream>
#include <fstream>

using namespace std;

int main()
{
int a;
int index = 0;

fstream fout;
fout.open("test.txt", ios::app);

/*
//可以直接这么写,程序会默认打开
fstream fout("test.txt");
*/

if(fout.fail()) //也可以判断fout是否非空:if(!fout)
{
cout << "open fail" << endl;
}

while(cin >> a)
{
fout << "The numbers are:" << a << endl; //用定义的fout输出到文件
index++;
if(index == 5)
{
break;
}
}

fout.close(); //关闭文件
}

  

文件打开方式 行为
ios::in ifstream的默认模式,打开文件进行读操作
ios::out ofstream的默认方式,打开文件进行写操作
ios::ate 打开一个已经有输入或输出文件并查找到文件尾
ios::app 打开文件以便在文件的尾部添加数据
ios::nocreate 如果文件不存在,则打开操作失败
ios::trunc 如果文件存在,清除文件原有内容(默认)
ios::binary 以二进制方式打开

  

  文件的默认打开方式是ASCII,如果需要以二进制方式打开,需要设置ios::binary:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
//二进制的方式拷贝一个文件:

#include <string>
#include <fstream>
#include <iostream>
using namespace std;

static const int bufferLen = 2048;
bool CopyFile(const string& src, const string& dst)
{
// 打开源文件和目标文件
// 源文件以二进制读的方式打开
// 目标文件以二进制写的方式打开
ifstream in(src.c_str(), ios::in | ios::binary);
ofstream out(dst.c_str(), ios::out | ios::binary | ios::trunc);

// 判断文件打开是否成功,失败返回false
if (!in || !out)
{
return false;
}

// 从源文件中读取数据,写到目标文件中
// 通过读取源文件的EOF来判断读写是否结束
// 分块读取,不要一下全读进来,防止缓冲区不够用
char temp[bufferLen];
while (!in.eof())
{
in.read(temp, bufferLen);
streamsize count = in.gcount(); //实际读的大小
out.write(temp, count);
}

// 关闭源文件和目标文件
// 如果不关闭会导致资源泄露
in.close();
out.close();

return true;
}

int main()
{
CopyFile("AA.mp3", "BB.mp3");

return 0;
}

I/O流

  传统的C语言中I/O处理有printf,scanf,getch,gets等函数,他们的问题是不可编程,仅仅能识别内置的数据类型,无法识别自定义的数据类型;而且代码移植性差,有很多坑。

  C++中有I/O流istream,ostream等处理方式,可编程,对于类库的设计者来说很有用;而且还能简化编程,使I/O的风格保持一致。

IO缓存区

  计算机发展过程中,需要实现外部的设备可以用统一的标准和计算机程序进行交互。最早的UNIX系统是以文件的方式进行交互。这个思想发展到现在就是IO缓存区。因为外部设备和内存的速度是不一样的,缓存区的存在可以让我们的信息读取更加高效。

标准的IO提供三种类型的缓存模式:

  1. 按块缓存:如文件系统,一次性加载到内存中
  2. 按行缓存:以''区分行
  3. 不缓存
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int main()
{
int a;
int index = 0;

while(cin >> a)
{
cout << "The numbers are:" << a << endl;
index++;
if(index == 5)
{
break;
}
}

char ch;
cin >> ch;
cout << "the last char is:" << ch << endl;

return 0;
}

  上面代码一连输入6个int,内部接收完5个数字后循环结束,但可以看到6还是读入了,并且以char的形式输出,后面的ch还没来得及输入程序就退出了。这是程序中面临的一个问题,输入的6被暂存到缓冲区中成为了脏数据,在我们不之情的情况下把后面的输入给覆盖掉了。

我们可以使用cin::ignore(int count,type metadelim)方法来清空缓冲区,第一个参数是要清空多少缓冲区信息,第二个参数表达以什么符号作为结尾

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//清空缓冲区
int main()
{
int a;
int index = 0;

while(cin >> a)
{
cout << "The numbers are:" << a << endl;
index++;
if(index == 5)
{
break;
}
}

//清空缓冲区脏数据
//numeric_limits<std::streamsize>::max()表示缓冲区的最大范围
cin.ignore(numeric_limits<std::streamsize>::max(), '\n');

char ch;
cin >> ch;
cout << "the last char is:" << ch << endl;

return 0;
}

  C++使用struct、class来定义一个类:struct的默认成员权限是public,class的默认成员权限是private;除此之外,二者基本无差别。

1
2
3
4
5
6
7
8
9
class Student{
private:
string name;
double score;
public:
double GetSorce(){
return score;
}
};

上面学生的类并不是真实世界中的学生,只是一个抽象的概念,并不包含真实世界中学生的所有属性,只是把一些属性抽象出来。

  

  面向对象的误区:对象是对现实世界中具体物体的反映,继承是对物体分类的反映?这个观念是错误的。举个例子,现实生活中我们往往把正方形看作是长和宽都相等的特殊的长方形,如果把这个思想引入到C++中,可能会这么设计继承关系:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class rectangle  //长方形
{
virtual void SetLength(double a)
{
……
}
};

class square : public rectangle //正方形,继承长方形类
{
virtual void SetLength(double a)
{
……
}
}

  上面设计了两个类,一个长方形类,一个正方形类,其中正方形类继承了长方形类以及内部的方法SetLength。当我们调用长方形类SetLength的方法时,我们只是修改了长方形的长,长方形的宽不受影响;但是如果我们调用了正方形对象的SetLength方法,不仅长会受到影响,宽也会受到影响。这个从面向对象的继承体系来说就有很大的问题了。所以我们不要把现实世界中的关系代入到面向对象编程中。

  

抽象法则

具体类型的抽象

  • 让自定义的类像内置类型一样

  一个普通的int型变量,可以完成加、减、乘、除、比较、输出、自增等等一系列操作;如果现在有一个自定义的复数类型,我们自然也希望可以像使用int型变量一样使用它,同时它对我们是一个黑盒,一种抽象,我们不需要关心内部是如何实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
//complex.h
#pragma once

#include <iostream>
using namespace std;

class Complex
{
public:
Complex(); // 默认构造函数
Complex(double r, double i); // 构造函数
virtual ~Complex(); // 析构函数
Complex(const Complex& x); // 拷贝构造
Complex& operator=(const Complex &c); // =号运算符


double GetReal( ) const { return _real; } //const修饰函数,函数体内成员变量不允许改变
void SetReal(double d) { _real = d; }
double GetImage() const { return _image; }//const修饰函数,函数体内成员变量不允许改变
void SetImage(double i) { _image = i; }

// 运算符重载
//这里函数参数传递的是引用,因为可能从外面传递另外一个complex,但我们不想生成一个副本
//重载等号操作符时返回的是引用,可以让返回值在其他地方使用
Complex operator+(const Complex &c) const;
Complex& operator+=(const Complex &c);
Complex operator-(const Complex &c) const;
Complex& operator-=(const Complex &c);
Complex operator*(const Complex &c) const;
Complex& operator*=(const Complex &c);
Complex operator/(const Complex &c) const;
Complex& operator/=(const Complex &c);
Complex& operator=(const Complex &c);

bool operator==(const Complex &c) const;
bool operator!=(const Complex &c) const;
bool operator>(const Complex &c) const;
bool operator>=(const Complex &c) const;
bool operator<(const Complex &c) const;
bool operator<=(const Complex &c) const;


// 前置和后置++
Complex& operator++(); //前置++
Complex operator++(int); //后置++
Complex& operator--(); //前置--
Complex operator--(int); //后置--


//标准输入输出IO重载
//输入输出需要把当前的Complex对象传递进来,也要把外部的ostream或istream传递进来
//但是传递进来的ostream本身不是当前类内的成员变量,不能访问当前类的属性
//所以定义为friend
friend ostream& operator<<(ostream& os, const Complex &x);
friend istream& operator>>(istream& is, Complex &x);

private:
double _real; // 复数的实部
double _image; // 复数的虚部
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
//complex.cpp
#include "stdafx.h"
#include "complex.h"

Complex::Complex()
{
_real = 0.0;
_image = 0.0;
//cout << "Complex::Complex()" << endl;
}

Complex::Complex(double r, double i)
{
_real = r;
_image = i;
//cout << "Complex::Complex(double r, double i)" << endl;
}

Complex::Complex(const Complex& c)
{
_real = c._real;
_image = c._image;
//cout << "Complex::Complex(const Complex& c)" << endl;
}

Complex& Complex::operator= (const Complex& c)
{
if (this != &c) //如果二者相等,就不需要做操作,这里比较的是地址
{
_real = c._real;
_image = c._image;
}
return *this;
}

Complex::~Complex()
{
_real = _image = 0.0;
//cout << "Complex::~Complex()" << endl;
}

Complex Complex::operator+ (const Complex& c) const
{
//Complex tmp;
//tmp._real = _real + c._real;
//tmp._image = _image + c._image;
//return tmp;

//上面的写法,在return的时候会触发拷贝构造
//调试打断点可以发现,return会进入“=”操作符的重载,重载函数入参的地址和tmp这个临时对象的地址不一样
//这个tmp被传递到拷贝构造里了

return Complex(_real + c._real, _image + c._image);
//临时对象优化,上面的写法就不会触发拷贝构造,直接走了自定义的构造函数
}

Complex& Complex::operator+= (const Complex& c)
{
_real += c._real;
_image += c._image;

return *this;
}

Complex Complex::operator-(const Complex &c) const
{
return Complex(_real - c._real, _image - c._image);
}

Complex& Complex::operator-=(const Complex &c)
{
_real -= c._real;
_image -= c._image;

return *this;
}

Complex Complex::operator*(const Complex &c) const
{
return Complex(_real*c._real - _image*c._image, _real*c._image + _image*c._real);
}

Complex& Complex::operator*=(const Complex &c)
{
Complex tmp(*this); //拷贝构造函数
_real = tmp._real*c._real - _image*c._image;
_image = tmp._real*c._image + tmp._image*c._real;
return *this;
}

Complex Complex::operator/(const Complex &c) const
{
double t = c._real*c._real + c._image*c._image;
return Complex((_real*c._real - _image*(-c._image)) / t, (_real*(-c._image) + _image*c._real) / t);
}

Complex& Complex::operator/=(const Complex &c)
{
Complex tmp(*this); //拷贝构造函数
double t = c._real*c._real + c._image*c._image;
_real = (tmp._real*c._real - tmp._image*(-c._image)) / t;
_image = (tmp._real*(-c._image) + tmp._image*c._real) / t;
return *this;
}

bool Complex::operator==(const Complex& c) const
{
return (_real == c._real) && (_image == c._image);
}

bool Complex::operator!=(const Complex& c) const
{
return !( (_real == c._real) && (_image == c._image) );
}

bool Complex::operator>(const Complex &c) const
{
return (_real > c._real) && (_image > c._image);
}

bool Complex::operator>=(const Complex &c) const
{
return (_real >= c._real) && (_image >= c._image);
}

bool Complex::operator<(const Complex &c) const
{
return (_real < c._real) && (_image < c._image);
}

bool Complex::operator<=(const Complex &c) const
{
return (_real <= c._real) && (_image <= c._image);
}

Complex& Complex::operator++ () // 前置++
{
_real++;
_image++;
return *this;
}

Complex Complex::operator++ (int) // 后置++
{
//先用中间变量,然后执行计算,之后把原始值传递出去
//Complex tmp(*this);
//_real++;
//_image++;
//return tmp;

//上面的方法会调用拷贝构造,使用下面方法优化
return Complex(_real++, _image++);
}

Complex& Complex::operator--() //前置--
{
_real--;
_image--;
return *this;
}

Complex Complex::operator--(int) //后置--
{
return Complex(_real--, _image--);
}


//注意输入输出的写法,虽然在类内声明了,但本身并不是类的成员
//所以定义为全局函数,不要加“Complex::”
ostream& operator<<(ostream& os, const Complex &x)
{
os << "real value is " << x._real << " image value is " << x._image;
return os;
}

istream& operator >> (istream& is, Complex &x)
{
is >> x._real >> x._image;
return is;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
int main()
{

Complex a(1.0, 2.0);
cout << a.GetReal() << endl; // 1.0
cout << a.GetImage() << endl; // 2.0
a.SetImage(2.0);
a.SetReal(3.0);
cout << a.GetReal() << endl; // 3.0
cout << a.GetImage() << endl; // 2.0



Complex a(3.0, 2.0);

//系统也会默认有等号运算符的重载,我们自己不实现也可以用
Complex c;
c = a + b;

//注意上面的写法和Complex c = a + b;的含义是不一样的
//上面的行为是先声明后赋值
//Complex c = a + b的行为是定义
//调试时需要区别,如果要调试赋值操作,不要写成定义流程了

//实际工程中尽量写Complex c = a + b;这个,减少一个默认构造函数提高效率


Complex d;
d = c++;
cout << d << endl;
cin >> d;
cout << d << endl;

return 0;
}

  类中有个this指针,指向当前对象本身。

  类创建后会系统默认创建一个构造函数,我们可以自己实现构造函数。但我们如果重写了构造函数,那么原始的默认构造函数就不存在了,如果想使用需要重新声明实现。

  等号运算符也一样,系统会默认帮我们重载。不过我们最好不要过于相信系统默认的重载,在复杂情况下运算的结果可能不是我们想要的。

  程序中的临时对象一定要注意优化,避免产生临时对象,否则会触发拷贝构造。

  

抽象类型的抽象

  数学中有不同的图形,比如长方形、原型、三角形;多种图形计算周长、面积的方法不同,但都需要一个计算方法。我们可以抽象出一个图形类Shape,用Shape类进行公共层面的抽象操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
#include "stdafx.h"
#include <iostream>
using namespace std;

// 抽象类
// 抽象类的特征是虚函数后面都是"=0",意味着不可以有实际的对象
class Shape
{
public:
virtual double Area() const = 0;// 子类方法实现不一致时要加上virtual
virtual void Show() = 0;
void SetColor(int color) { _color = color; }
void Display() //一个共性的方法
{
cout << Area() << endl;
}
private:
int _color;
};

class Square: public Shape
{
public:
Square(double len) :_len(len) { } //用参数列表初始化
void Show() { cout << "Square" << endl; }
double Area() const
{
return _len*_len;
}
private:
double _len; //边长
};


class Circle : public Shape
{
public:
Circle(double radius) :_radius(radius) {} //用参数列表初始化
void Show() { cout << "Circle" << endl; }
double Area() const
{
return 3.1415926*_radius*_radius;
}

private:
double _radius; //半径
};

class Triangle : public Shape
{
public:
Triangle(double len, double height) :_len(len), _height(height){}
void Show() { cout << "Triangle" << endl; }
double Area() const
{
return 0.5*_len*_height;
}

private:
double _len; //底
double _height; //高
};

int main()
{
// 多态的实现,可以让我们面对变化,尽可能少修改原有的逻辑,直接扩充逻辑
const int shapeNum = 3;

Square s1(2.0);
s1.SetColor(1);
Circle c1(2.0);
Triangle t1(2.0, 3.0);

Shape* shapes[shapeNum];
shapes[0] = &s1;
shapes[1] = &c1;
shapes[2] = &t1;

for (unsigned int index = 0; index < shapeNum; index++)
{
shapes[index]->Display();
}

cout << endl;
cout << sizeof(s1) << endl;

return 0;

}

对象模型和虚函数

  C++的对象模型中,子类对象中包含了父类。父类中有一个虚函数列表,是个类似数组的结构。对象模型中只保留成员变量信息和虚函数列表,其他的共有函数是通过this指针来访问的。

深拷贝、浅拷贝、写时复制

  • 浅拷贝:只拷贝指针地址,C++默认拷贝构造函数与赋值运算符重载都是浅拷贝;节省空间,但容易引发多次释放;
  • 深拷贝:重新分配堆内存,拷贝指针指向内容;浪费空间,但是不会导致多次释放;

  深拷贝的思想比较常见,比如C++的一个优化策略叫写时复制。有个信息存放在内存空间中,如果大家都去读取,那么内存中保留一份即可,但如果有地方需要写数据,那么会复制出一个新的地址空间存放相同数据,写操作作用在新地址空间上。

  深拷贝和浅拷贝各有优劣,如果想兼有二者的优点,有两种可用方案:第一是使用引用计数,用shared_ptr的思路,每有一个指针指向对象,引用计数+1,直到引用计数清零时再清理内存;第二种是C++11的新标准移动语义move,把资源让渡,既可以避免重新创建空间,也防止空间释放导致新问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
//自定义字符串类String操作

class String
{
public:
String(const char *str = NULL); // 普通构造函数
String(const String &other); // 拷贝构造函数(深拷贝)
String(String&& other); // 移动构造函数,两个&&代表右值引用
~String(void); // 析构函数
String& operator= (const String& other); // 赋值函数
String& operator=(String&& rhs)noexcept; // 移动赋值运算符

friend ostream& operator<<(ostream& os, const String &c); // cout输出

private:
char *m_data; // 用于保存字符串
};


// String 的普通构造函数
String::String(const char *str)
{
if (str == NULL)
{
m_data = new char[1];
if (m_data != NULL)
{
*m_data = '\0';
}
else
{
exit(-1);
}
}
else
{
int len = strlen(str);
m_data = new char[len + 1]; //把'\0'的空间留出来
if (m_data != NULL)
{
strcpy(m_data, str);
}
else
{
exit(-1);
}
}
}

// 拷贝构造函数
// 开辟新的内存空间了,属于深拷贝
String::String(const String &other)
{
int len = strlen(other.m_data);
m_data = new char[len + 1];
if (m_data != NULL)
{
strcpy(m_data, other.m_data);
}
else
{
exit(-1);
}
}

// 移动构造函数
String::String(String&& other)
{
if (other.m_data != NULL)
{
// 资源让渡
m_data = other.m_data;
// 之前的联接断开
other.m_data = NULL;
}
}

// 赋值函数
String& String::operator= (const String &other)
{
if (this == &other)
{
return *this;
}
// 释放原有的内容
delete[] m_data;
// 重新分配资源并赋值
int len = strlen(other.m_data);
m_data = new char[len + 1];
if (m_data != NULL)
{
strcpy(m_data, other.m_data);
}
else
{
exit(-1);
}

return *this;
}

// 移动赋值运算符
String& String::operator=(String&& rhs)noexcept
{
if(this != &rhs)
{
// 清空原有的
delete[] m_data;

//赋值新的
m_data = rhs.m_data;

// 我们接管了内存,原有内存不需要释放,把链接断开即可
rhs.m_data = NULL;
}
return *this;
}

// String 的析构函数
String::~String(void)
{
if (m_data != NULL)
{
delete[] m_data;
}
}

ostream& operator<<(ostream& os, const String &c)
{
os << c.m_data;
return os;
}

int main()
{
String s1("Hello"); // 构造函数
cout << s1 << endl;
//String s2 = s1; // 调用拷贝构造函数
String s2(s1); // 调用拷贝构造函数
cout << s2 << endl;
String s2A(std::move(s1)); // 移动构造函数
cout << s2A << endl;
String s3; // 无参构造函数
cout << s3 << endl;
s3 = s2; // 调用赋值函数
cout << s3 << endl;
String s3A; // 无参构造函数
s3A = std::move(s2A); // 移动赋值运算符
cout << s3A << endl;

return 0;
}

面向对象三大特性

  • 封装性:数据和代码捆绑在一起,避免外界干扰和不确定性访问;封装可以使得代码模块化;
  • 继承性:让某种类型对象获得另一个类型对象的属性和方法,继承可以扩展已经存在的代码;
  • 多态性:同一事物在保有原来特点的情况下表现出不同事物的能力,即不同对象会产生不同的行为;多态的目的是为了接口重用;

  面向对象是软件工程发展到一定阶段为了管理代码和数据提出的一种方法,它没有解决以前解决不了的问题,不是万能的,只是为我们便捷的开发出能适应快速变化的软件提供了可能。面向对象不是对现实世界的映射,但它的封装性可以把问题简化;它的继承性可以减少代码重复,避免重新发明轮子;它的多态可以实现灵活的功能扩充,提升开发效率;

ASCII码表

  ASCII(American Standard Code for Information Interchange,美国信息交换标准代码)是基于拉丁字母的一套电脑编码系统,主要用于显示现代英语和其他西欧语言。它是最通用的信息交换标准,并等同于国际标准ISO/IEC 646。

   ASCII码使用指定的7位或8位二进制数组合来表示128或256种可能的字符。美国一开始设计的是7位共128个,包含95个可见字符和33个控制字符,后期扩展到8位,称为扩展ASCII码。

  

国标编码

  中国也有自己的编码,用于表示中文字符。设计编码首先要确定字符集。国标使用分区管理,共计分为94个区,每个区含94位,共8836个码位。19区收录除汉字外的682个字符;1015区为空白区,没有使用;1655区收录3755个一级汉字,按照拼音排序;5687区收录3008个二级汉字,按照部首/笔画排序;剩余的88~94区为空白区,没有使用。这套字符集称为GB2312字符集。按照这个字符集来编码,比如3区的6行7列字符,编码0367。

  ASCII是直接把码位按照二进制存储,但国标编码不是。比如5709编码的汉字(侃),把6709按照前两位和后两位分开,并分别转为16进制:0x39和0x09;然后两个数分别加上0xA0得到0xD9和0xA9,将两数合并,0xD90xA9就是汉字侃的GB2312码。

  GB2312编码之所以要加0xA0,是为了要兼容ASCII码。GB2312是双字节编码,为了与ASCII编码区分,每个单字节的第八位必须是1,所以GB2312编码最少要从0x80(1000 0000)开始。但是根据规定,0x80~0x9F需要留给控制块,所以只能从0xA0开始。

  计算机判断字符是ASCII码还是GB2312编码的方式就是通过字符大小,ASCII码是7个字节,如果小于127就是ASCII码,如果连续碰到两个大于127的8位就把两个组合起来当作GB2312编码。

  GB2312编码只能表示6763个汉字,后期也不够用了,所以对GB2312做了扩展为GBK字符集。GB2312的高位和地位都要求必须大于127,GBK不再规定低位大于127,只保证高位大于127。把之前没用上的码位都用上,扩充了将近20000个汉字和符号。计算机只要碰到一个大于127的字节,就表示一个汉字的开始。

  后期还扩展了GB18030字符集,新增了少数民族的字符。

  

Unicode

  ASCII可表示的字符数太少了,但如果每个国家都设计一套自己的编码就太乱了,为了把世界上的文字都映射到一套字符空间中,诞生了Unicode。Unicode是一个标准,规定了字符集和编码。

  最开始的Unicode字符集称为UCS-2字符集,和ASCII码一样,把用到的字符按顺序罗列并标上对应的码位。存储方式和ASCII一样,直接把码位按照二进制方式存储。一共可以表示即65536个字符。后期发现不够用后出现了UCS-4字符集,可以表示将近43亿个字符,但由于这个编码占用的存储空间太大了,并没有被广泛接受。

  互连网时代后,对Unicode做了优化,目前有3种Unicode的编码方式:

  1. UTF-8:用1byte来表示字符,可以兼容ASCII码;
    1. 特点是存储效率高,可变长(不方便内部随机访问);
    2. 无字节序问题,可以作为外部编码;
  2. UTF-16:用2bytes表示一个字符,可以分为UTF-16BE(big endian)和UTF-16LE(little endian)
    1. 特点是定长的,方便内部随机访问;
    2. 有字节序问题,不可以作为外部编码;
  3. UTF-32:用4bytes表示一个字符,可以分为UTF-32BE(big endian)和UTF-32LE(little endian)
    1. 特点是定长的,方便内部随机访问;
    2. 有字节序问题,不可以作为外部编码;

   注意:如果使用的Windows系统,Windows文件可能有BOM(byte order mark)来表示字节序,如果要在其他平台使用,可以去掉BOM或者忽略掉。BOM在文本文件头部,FEFF(十进制为254 255)表示大端,FFFE(十进制为255 254)表示小端。

  

  UTF-8编码原理:UTF-8将UCS-4字符集的码位划分为4个区间(0x000000000x0000007F;0x000000800x000007FF;0x000008000x0000FFFF;0x000100000x0010FFFF),第一个区间的编码样式为0XXXXXXX,第二个区间的编码样式为110XXXXX 10XXXXXX,第三个区间的编码样式为1110XXXX 10XXXXXX 10XXXXXX,第四个区间的编码样式为11110XXX 10XXXXXX 10XXXXXX 10XXXXXX。

  汉字“王”在UCS-4中的编码为0x0000738B,转换为二进制就是“0000 0000 0000 0000 0111 0011 1000 1011”。0x0000738B属于UCS-4的第三区间,这个区间的编码样式是“1110XXXX 10XXXXXX 10XXXXXX”。此时我们得到了“王”这个字符的二进制形式和编码样式,把二进制形式从高到低依次插入到编码样式中,得到了“王”对应的UTF-8编码:“11100111 10001110 10001011”,十六进制为“0xe7 0x8e 0x8b”。

  

程序中编码错误的根本原因在于编码方式和解码方式的不统一。

C语言字符的语法陷阱

先看C语言中常见的词法、语法问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
char c1 = 'yes';
/*
* 不符合常理,但这样定义没有错误
* 编译器会截断
* 至于是保留第一个还是最后一个,这个和编译器有关
* 虽然没报错,但编译器会有warning
*/


char c2 = "yes";
/*
* 编译器报错
* "yes"是一个字符串,c2只是一个字符变量,不能存储字符串
*/

const char* slash = "/";
/*
字符串的正确定义方法
slash中存放两个字符:'/'、'\0'
这样其实就是把字符串的首地址给了指针变量
*/

const char* slash2 = '/';
/*
编译器报错
字符的类型不能给指针,两个变量类型不匹配
*/

const char* slash3 = &c1;
/*
正确
slash3指针变量存放c1单个字符的地址
*/

  从上面的例子可以看到,C语言是高级语言中的低级语言,优点是小巧、高效、接近底层,比如上面的例子就把字符和字符串区分的很细,但缺点就是细节和陷阱比较多。为了更好的解决这个问题,C++在兼容C语言的同时,推出了既高效又易于大规模开发的机制:string类的使用:

1
2
3
4
5
6
7
8
#include <string>

string s1(1,'yes'); //s
string s2(3,'yes'); //sss
string s3(1,'y'); //y
string s4("/"); // /
string s5(1,'/'); // /
string s6("yes"); //yes

C语言指针和数组的关系问题

c预言数组在作为参数时的退化行为,退化为一个指针。

给定一个数组,计算数组中的数据的平均数,有以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//计算平均数
double average1(int arr[10])
{
double result = 0.0;
int len = sizeof(arr) / sizeof(arr[0]);
std::cout << "In average1 : " << len << std::endl;
for (int i = 0; i < len; i++)
{
result += arr[i];
}

return result / len;
}

int main()
{
int array1[] = { 10,20,30,40,50,60,70,80,90,100 };

//数组长度最好是用变量这样来求,不要写成常量
//这样方便扩展
int len = sizeof(array1) / sizeof(array1[0]);
std::cout << "len : " << len << std::endl;

std::cout << average1(array1) << std::endl;

return 0;
}

  可以看到输出的值并不是平均数,通过输出中间数据可以知道,main函数中的长度是10,而average1中的数组长度是1;

  出现这个的原因就是C预言数组在作为函数参数传递时会退化为一个指针,average1中的入参实际上只是函数的首地址,sizeof(arr)输出的只是单个元素的长度。

  可以进行如下优化,通过外部把数组长度先行计算出来然后传递给函数。需要注意,如果传递的是字符数组的话就不需要这么麻烦了,因为字符数组往往是通过'\0'结尾的,函数内部有办法知道数组的长度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//直接把数组长度传递进来
double average2(int arr[10], int len)
{
double result = 0.0;

for (int i = 0; i < len; i++)
{
result += arr[i];
}

return result / len;
}

int main()
{
int array1[] = { 10,20,30,40,50,60,70,80,90,100 };

int len = sizeof(array1) / sizeof(array1[0]);

std::cout << average2(array1,len) << std::endl;

return 0;
}

  

  其实知道数组当作函数参数传递时会发生退化时,就可以不传递数组,而是只传递指针:

1
2
3
4
5
6
7
8
9
10
11
double average2(int* arr, int len)
{
double result = 0.0;

for (int i = 0; i < len; i++)
{
result += arr[i];
}

return result / len;
}

  

  C语言之所以要这么做,是和c语言发展分不开的。c语言早期是伴随着unix操作系统,是非常底层的,对空间要求非常高的语言。如果函数传参时传递了一个非常大的数据容器,空间转移的效率是非常低的。所以C语言设计者就通过传递指针和容器尺寸这样一种传递方式从而达到节省空间的目的。

  

  C++的解决方案就是引入STL容器,实现底层包装,保证效率的同时也保证简单安全。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <vector>

//这里传递的是引用
//如果传递的是vector本身,c++这里会产生一个副本,如果容器很大会得不偿失
double average3(std::vector<int>& v)
{
double result = 0.0;
std::vector<int>::iterator it = v.begin();
for (;it!=v.end();++it)
{
result += *it;
}

return result / v.size();
}

int main()
{
std::vector<int> vt = { 10,20,30,40,50,60,70,80,90,100 };
std::cout << average3(vt) << std::endl;

return 0;
}

  使用stl容器后,哪怕是二维数组,处理起来也很方便了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
double average2DV(vector<vector<int> >& vv)
{
double result = 0.0;
unsigned int size = 0;

for (unsigned int i = 0; i < vv.size(); ++i)
{
for (unsigned int j = 0; j < vv[i].size(); ++j)
{
result += vv[i][j];
size += 1;
cout << vv[i][j] << " ";
}
cout << endl;
}

return result / size;
}

int main()
{
vector<vector<int> > vv2D{8, vector<int>(12,3) }; //8个vector,每个包含12个3
cout << average2DV(vv2D);
return 0;
}

C语言的移位问题

问题一:右移操作:无法区分是逻辑右移还是算术右移。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include<cstdio>

#include <bitset>
#include <iostream>
using namespace std;

int main()
{

char a1 = 0x63; // 0110 0011
a1 = (a1 << 4); // 0011 0000
printf("0x%x\n", a1); //左移操作,末位补0

a1 = 0x63; // 0110 0011
a1 = (a1 >> 4); // 0000 0110 逻辑右移
printf("0x%x\n", a1); // 逻辑右移自动补0


char a2 = 0x95; // 1001 0101
a2 = (a2 << 4); // 0101 0000
printf("0x%x\n", a2); //左移操作,末位补0

a2 = 0x95; // 1001 0101
a2 = (a2 >> 4); // 1111 1001 算术右移
printf("0x%x\n", a2); //这里执行的是算数右移操作,补1了

return 0;
}

  上面可以看到,C语言在执行右移操作时表现不同,而不同的编译器输出的结果可能都不一样,C语言并没有做统一标准。C语言官方的做法是在做右移操作时,把操作数都变为无符号的数,这样可以保证执行的是逻辑右移操作(补0)。原因是无符号数首位都是0,可以保证补位的数也是0。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int main()
{

unsigned char a3 = 0x63; // 0110 0011
a3 = (a3 << 4); // 0011 0000
printf("0x%x\n", a3);

a3 = 0x63; // 0110 0011
a3 = (a3 >> 4); // 0000 0110 逻辑右移
printf("0x%x\n", a3);


unsigned char a4 = 0x95; // 1001 0101
a4 = (a4 << 4); // 0101 0000
printf("0x%x\n", a4);

a4 = 0x95; // 1001 0101
a4 = (a4 >> 4); // 0000 1001 逻辑右移
printf("0x%x\n", a4);

return 0;
}

  

  

问题二:移位操作位数的限制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int main()
{
//示例常见与权限控制,每一位代表不同的权限
//0000 0000
const unsigned char priv = 0xFF; //初始化权限
const unsigned char P_BAKCUP = (1<<7); //备份权限
const unsigned char P_ADMIN = (1<<8); //最高权限

printf("0x%x\n", P_BAKCUP);
printf("0x%x\n", P_ADMIN);
if (priv & P_BAKCUP)
{
cout << "BAKUP" << endl;
}
if (priv & P_ADMIN)
{
cout << "ADMIN" << endl;
}
return 0;
}

  由运行结果可以看到,char本身就只有8位,P_ADMIN的移位操作已经超过了8位,这时候所有的8位都被清零了。这是C语言编码常见错误,移位操作一定要注意操作位数上限,移位数大于0,小于位数;

  

  出现上面两个问题的原因就是,C语言设计移位操作时需要考虑操作数表示的上下文环境。C++为了对这个问题做改进,引入了bitset:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <bitset>

int main()
{
// bitset
bitset<10> priv = 0xFF; //手动控制,这里就只有10位
bitset<10> P_BAKCUP = (1 << 6);
bitset<10> P_ADMIN = (1 << 7);

//这里可以直接输出
cout << priv << endl;
cout << P_BAKCUP << endl;
cout << P_ADMIN << endl;

if ((priv & P_BAKCUP) == P_BAKCUP)
{
cout << "BAKUP" << endl;
}
if ((priv & P_ADMIN) == P_ADMIN)
{
cout << "ADMIN" << endl;
}

return 0;
}

C语言强制类型转换问题

C语言中强制类型转换隐藏了很多bug和陷阱:

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 main()
{
int array[] = { 1,2,3 };
cout << sizeof(array) / sizeof(array[0]) << endl;
int threshold = -1;

if (sizeof(array) / sizeof(array[0]) > threshold)
{
cout << "positive number array" << endl;
}
else
{
cout << "negative number array" << endl;
}

return 0;
}

  上面的代码当数组长度大于0时,需要输出“positive number array”,否则输出“negative number array”。可以通过编译运行后,长度输出为3是正确的,但判断逻辑里却输出了“negative number array”。

  发生这个问题的原因是sizeof的返回值是unsigned int,是无符号数,但threshold却是一个有符号数,在执行比较判断语句时,C语言的机制把threshold转换为了一个无符号数,然后才进行的比较。这里发生的是隐式类型转换。-1转换为unsigned int时会变为4294967295,是个很大的正整数(这里涉及到了补码转换)。

  C语言在编写时,可以先用一个有符号的数把数据先取出来。今后编码时也需要注意,尽量避免用无符号的数据来进行数据比较:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
using namespace std;

int main()
{
int array[] = { 1,2,3 };
cout << sizeof(array) / sizeof(array[0]) << endl;
int threshold = -1;
int len = sizeof(array) / sizeof(array[0]); //用一个有符号的变量先把数据拿出来

if (len > threshold)
{
cout << "positive number array" << endl;
}
else
{
cout << "negative number array" << endl;
}

return 0;
}

  

  

类型转换还可能会发生在以下情况:假设要计算1+1/2+1/3+……+1/n,如果代码是这么写的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 1+1/2+1/3+1/4+... +1/n
double getSum(int n)
{
double result = 0.0;
for (int i = 1; i < n + 1; i++)
{
result += 1 / i;
}
return result;
}

int main()
{
int n = 0;
cin >> n;
cout << getSum(n) << endl;

return 0;
}

可以看到,计算出的结果是1。这里的问题出在“result += 1/n”这句中,被除数是整形,除数也是整形,那么计算结果也是整型值。result虽然会转换为浮点数,但整形计算中已经丢失了精度。

c语言中的一个解决方法是把被除数先转换为浮点数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 1+1/2+1/3+1/4+... +1/n
double getSum(int n)
{
double result = 0.0;
for (int i = 1; i < n + 1; i++)
{
result += 1.0 / i; //把被除数换成浮点数,那么结果会被转换为浮点数
}
return result;
}

int main()
{
int n = 0;
cin >> n;
cout << getSum(n) << endl;

return 0;
}

  

  

  上面两个例子可以看到,有时候我们会忽略C语言的隐式类型转换,导致出现程序bug;但有时候我们又需要这种隐式类型转换来得到我们想要的结果。c语言中滥用类型转换可能导致灾难性的后果,且很难排查。C语言之所以这么设计,是因为类型转换在底层语言中的运用非常广泛,且灵活方便。C++为了方便排查隐藏bug减少复杂性,提供了四种类型转换的方式:static_cast、const_cast、dynamic_cast、reinterpret_cast

  • static_cast:其实就是类似于C语言中的类型转换,C++提供了这么一种标准格式用于显示类型转换,可以方便程序员精准定位程序哪里使用了强制类型转换;
  • const_cast:只针对去除const属性;
  • dynamic_cast:用于类的继承关系转换,比如把子类转换为父类、或者父类转换为子类;
  • reinterpret_cast:用于指针的转换;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 1+1/2+1/3+1/4+... +1/n
double getSum(int n)
{
double result = 0.0;
for (int i = 1; i < n + 1; i++)
{
result += static_cast<double>(1) / i;
}
return result;
}

int main()
{
int n = 0;
cin >> n;
cout << getSum(n) << endl;

return 0;
}

C语言的整数溢出问题

  32位系统中,一个整数占用4个字节,共32位。其中第一位是符号位,所以一共有31位可以表示整数范围。如果计算的时候,如果我们算出的数值超出了数据表示范围,那么会数据溢出变为负数。要注意C语言中的整数不能和数学上的整数划等号。

1
2
3
4
5
6
7
8
9
10
11
int main()
{
int i = 2147483640;
for (; i > 0; i++)
{
cout << "adding " << i << endl;
}
cout << "exit " << i << endl;

return 0;
}

  出现这个问题的原因和系统的设计是有关的。数据存储空间是有限的,不能无限增长。C语言的一个解决方案是通过字符串的方式来表达大数的运算,字符串理论上是可以无限长的,C语言是有这个类库的,但并没有直接的解决方案。 C++本身也没有提供好的解决方案,但boost库中提供了cpp_int方法:boost官网

  

C语言字符串的典型缺陷

  C标准字符和字符串的区别是:字符是单引号括起来的,字符串是双引号括起来的,由'\0'结尾。而'\0'作为结束符这个方式,表达能力有天生的缺陷:一旦字符串中间具有'\0'字符,那么c语言的字符串函数就会认为这个字符已经结束了。如果用c语言的方式存储一些图片或者其他二进制的内容,很容易出问题。

  C语言的字符串操作还有另一个问题就是效率低下。C语言的字符处理函数都是通过遍历'\0'来寻找字符串结尾的,这个遍历操作会消耗性能。

  C语言设计这种字符串处理方式主要是为了节省空间,有针对性的设计问题。C++语言为了解决这个问题有以下几种思路:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
int main()
{
cout << "Testing C++ String: " << endl;
string sstr1 = "string";
cout << sstr1.length() << endl; //字符串内容的长度
cout << sstr1.capacity() << endl; //string的容量长度
cout << sizeof(sstr1) << endl; //实际内存分配长度,不同平台的值可能不一致,但实际大小肯定会大于内容长度

cout << endl;

cout << "sstr2: " << endl;
string sstr2 = "stri\0ng";
cout << sstr2.length() << endl;
cout << sstr2.capacity() << endl;
cout << sizeof(sstr2) << endl;

cout << endl;

cout << "sstr1: " << endl;
sstr1 += sstr2; //字符串直接拼接
cout << sstr1.length() << endl;
cout << sstr1.capacity() << endl;
cout << sizeof(sstr1) << endl;

return 0;
}

  可以看到,string类的实现方案中,内部不仅记录了字符串的内容,还有几个变量记录了字符串内容的长度、容量等,在执行字符串操作时不需要遍历寻找'\0',提高了效率;但是依旧保留了c风格字符串以'\0'结尾的传统,还具有一些缺陷。

机器数和真值

机器数:一个数在计算机中的二进制表示形式,叫做这个数的机器数。

机器数是带符号的,在计算机中用一个数的最高位存放符号,正数为0,负数为1;

比如:十进制数+3,就是00000000000000000000000000000011;十进制数-3,就是10000000000000000000000000000011;(int值占4字节);这个例子只是整型数,浮点数有其他表达方式。

真值:真正的数学意义上的数值。因为机器数第一位是符号位,所以机器数的形式就不等于真正的数值。

补码

按照上面的机器数的表示方法有一个问题,第一位用作符号位的话,这个数的表示范围会变小。所以计算机中存储用的并不是机器数,而是补码。

无符号数的编码

用一个函数(Binary to Unsigned的缩写,长度为w)来表示:

eg:

有符号数的补码

用一个函数(Binary to Two`s-complement的缩写,长度为w)来表示:

eg:

补码数值范围

8位字长 16位字长 32位字长 64位字长
UMax 0xFF
255
0xFFFF
65535
0xFFFFFFFF
4294967295
0xFFFFFFFFFFFFFFFF
18446744073709551615
TMin 0x80
-128
0x8000
-32768
0x80000000
-2147483648
0x8000000000000000
-9223372036854775808
TMax 0x7F
127
0x7FFF
32767
0x7FFFFFFF
2147483647
0x7FFFFFFFFFFFFFFF
9223372036854775807
-1
0
0xFF
0x00
0xFFFF
0x0000
0xFFFFFFFF
0X00000000
0xFFFFFFFFFFFFFFFF
0X0000000000000000

需要注意,无符号数中0xFFFFFFFF是最大值,但有符号数中这个值代表-1;还要注意有符号数中的最小值是0x80000000;

字节序(Byte Ordering)

以32位机器为例,一个字有32bits字长,占用4bytes,在内存中有以下两个存放方式:

  1. 大端法(Big Endian):大多数IBM机器、Internet传输;

  1. 小端法(Little Endian):Inter兼容机

个人机器基本上都是小端表示法。

补码的意义

  我们在设计软件系统时总是希望软件系统尽可能的简单通用。于是人们希望在只有加法运算器的情况下设计一种方法能实现减法运算。

  以时间为例:表盘一圈12个小时,现在是8点,那么3小时前是5点,9小时以后还是5点(8+9-12),这里进行的是模12的操作。所以8-3和8+9的结果是一样的;我们就可以用9来表示-3,如果想计算8-3,那么就用加法器计算8+9;

  当然单纯的这么想是有问题的,以时间为例我们可以得到一个对照表,遇见12就清零:

0 -1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11
0 11 10 9 8 7 6 5 4 3 2 1
  • 5-3 ==> (5+9)%12=2
  • 3-5 ==> (3+7)%12 = 10 ==>-2

  我们计算3-5的时候,得到的值是10,我们可以在对照表中得到10对应的值是-2,但我们计算5-3的时候,得到的值是2,我们却不需要找对应的值。计算机如何区分什么时候要找对应的值,什么时候不需要呢?

  可以对表进行一些修改:

0 -1 -2 -3 -4 -5 -6 5 4 3 2 1
0 11 10 9 8 7 6 5 4 3 2 1
  • 5-3 ==> (5+9)%12=2 ==>2
  • 3-5 ==> (3+7)%12 = 10 ==>-2

  表格修改后,每次计算完都在表格中进行对照,这样操作统一,得到的值也是正确的了。其实在计算机内部,补码的用处就是构造这张映射表的。