c++基础知识

用于记录 c++ 基础知识,学习资料来自狄泰软件学院可在淘宝购买学习

1、const 关键字

    c++ 在 c 的基础上对 const 做了进化处理,当碰见 const 声明时,在符号表中放入常量编译过程中发现使用常量则直接以符号表中的值替换编译过程如发现下述情况则给对应的常量分配储存空间

  • 对 const 全局常量使用 extern
  • 对 const 常量使用 & 操作符

注意
    c++ 编译器虽然可能会为 const 常量分配空间,但不会使用空间中的值。

1.1 符号表

    编译器在编译的过程当中所产生的一张表,是编译器内部的数据结构

    c++ 中 const 常量类似于宏定义const int c = 5;#define c 5

1.2 c++ 中的 const 与宏定义

    const 常量是被编译器处理,编译器对 const 常量进行类型检查作用域检查。宏定义由预处理器处理,是单纯的文本替换,宏没有类型以及作用域的概念, const 有作用域和类型的概念

2、布尔类型

    布尔类型占用一个字节的内存,取值有 true 和 false。true 代表真值,编译器内部用 1 表示。false 代表非真值,编译器内部用 0 表示。c++ 编译器会将非 0 值转换为 true , 0 值转换为 false

    布尔类型是 c++ 中的基础数据类型,因此可以定义 bool 类型的全局变量,可以定义 bool 类型的常量,可以定义 bool 类型的指针,可以定义 bool 类型的数组

3、C++ 中的引用

    变量是一段实际连续存储空间的别名,程序中通过变量来申请并命名存储空间,通过变量的名字可以使用存储空间。引用可以看作已经定义的变量的别名,引用的语法:type & name = var ; 例子:

1
2
3
int a = 4;
int & b = a; // b为a的别名
b = 5; // 操作b就是操作a

注意:普通引用在定义时必须用同类类型的变量进行初始化。

3.1 引用的意义

    引用做为变量的别名而存在,因此在一些场合可以代替指针。引用相对于指针了来说具有更好的可读性与实用性。swap函数对比

1
2
3
4
5
6
7
8
9
10
11
12
13
void swap(int& a, int& b)
{
int t = a;
a = b;
b = t;
}

void swap(int* a, int* b)
{
int t = *a;
*a = *b;
*b = *t;
}

注意:函数中的引用不需要初始化

3.2 特殊的引用(const引用)

    在 c++ 中可以声明 const 引用, 语法:const type& name = var; const 引用让变量拥有只读属性

1
2
3
4
5
6
int a = 4;
const int& b = a;
int* p = (int*)&b;

b = 5; // Error , 只读变量
*p = 5; // ok, 修改变量 a 的值

    当使用常量对 const 引用进行初始化时,c++ 编译器会为常量值分配空间,并将引用名作为这段空间的别名。

1
2
3
4
const int& b = 1;       //ok
int* p = (int*) &b;
b = 5; //Error , 只读变量
*p = 5; //ok , 修改变量a的值

    使用常量对 const 引用初始化后将得到一个只读变量,引用有自己的存储空间吗本质为指针。

3.3 关于 const 的疑问

    在编译期间不能直接确定初始值的 const 标识符,都会被做只读变量处理只有字面量初始化的 const 常量才会进入符号表使用其他变量初始化的 const 常量仍然是只读变量被 volatile 修饰的 const 常量不会进入符号表const 引用的类型与初始化变量的类型相同初始化变量成为只读变量不同则生成一个新的只读变量

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 <stdio.h>

int main()
{
const int x = 1; //常量值进入符号表,字面量初始化
const int& rx = x; //只读变量,相同类型

int& nrx = const_cast<int&>(rx); //普通变量

nrx = 5;

printf("x = %d\n", x); // 1
printf("rx = %d\n", rx); // 5
printf("nrx = %d\n", nrx); // 5
printf("&x = %p\n", &x); // 0xbf8e8a88
printf("&rx = %p\n", &rx); // 0xbf8e8a88
printf("&nrx = %p\n", &nrx); // 0xbf8e8a88

volatile const int y = 2; //只读变量
int* p = const_cast<int*>(&y);

*p = 6;

printf("y = %d\n", y); // 6
printf("p = %p\n", p); // 0xbf8e8a7c

const int z = y; //只读变量

p = const_cast<int*>(&z);

*p = 7;

printf("z = %d\n", z); // 7
printf("p = %p\n", p); // 0xbf8e8a74

char c = 'c';
char& rc = c;
const int& trc = c; //新的只读变量

rc = 'a';

printf("c = %c\n", c); // C = a
printf("rc = %c\n", rc); // rc = a
printf("trc = %c\n", trc); // trc = c

return 0;
}

3.4 引用的本质

    引用在 c++ 中的内部实现是一个指针常量

1
2
3
4
5
6
7
8
9
10
11
12
13
type& name;

void f(int& a)
{
a = 5;
}

Type* const name;

void (int * const a)
{
*a = 5;
}
  1. c++ 编译器在编译过程中用常量指针作为内部实现,因此引用所占用的内存空间大小与指针相同
  2. 从使用角度,引用只是一个别名,c++为了实用性而隐藏了引用的储存空间这一细节

    C++ 中的引用旨在在多数情况下代替指针、功能性:可以满足多数需要使用指针的场合、安全性:可以避开大部分由于指针操作不当而带来的内存错误、操作性:简单易用,又不失功能强大

  • 绝对不要返回动态局部变量的引用

3.5 引用与指针

    指针指针是一个变量,值为一个内存地址,不需要初始化,可以保存不同的地址,通过指针可以访问对应内存地址中的值,指针可以被const修饰成为常量或者只读变量。引用引用只是一个变量的新名字,对引用的操作(赋值,取地址等)都会传递到代表的变量上;const引用使其代表的变量具有只读属性;引用必须在定义时初始化,之后无法代表其他变量。

    从 c++ 语言的角度来看引用与指针没有任何关系;引用是变量的新名字,操作引用就是操作对应的变量。从 c++ 编译器的角度来看为了支持新概念“引用”必须要有一个有效的方案;在编译器内部,使用指针常量来实现“引用”;因此,“引用”在定义时必须初始化

    在工程项目开发中当c++编译时,直接站在使用的角度看待引用,与指针毫无关系,引用就是变量的别名,当对c++代码进行调试分析时,一些特殊情况,可以考虑站在c++编译器角度来看待引用

    C++不支持引用数组数组的本质为一段连续的内存空间,引用会打断这种连续性,c++为了更好的兼容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
#include <stdio.h>

int a = 1;

struct SV
{
int& x;
int& y;
int& z;
};

int main()
{
int b = 2;
int* pc = new int(3);
SV sv = {a, b, *pc};
int& array[] = {a, b, *pc}; // &array[1] - &array[0] = ? Expected ==> 4

printf("&sv.x = %p\n", &sv.x);
printf("&sv.y = %p\n", &sv.y);
printf("&sv.z = %p\n", &sv.z);

delete pc;

return 0;
}

4、三目运算符( a < b ? a : b )

    c 语言中的三目运算符返回的是变量值 不能作为左值使用, c++ 中三目运算符可直接返回变量本身 即可作为右值,也可作为左值使用 当三目运算符的可能返回都是变量时,返回的是变量的引用。当三目运算符可能返回的值中有常量时,返回是变量的值。因此三目运算符可能返回值的值如果有一个值是常量值,则不能作为左值使用。例子:

1
2
3
4
5
int a = 0;
int b = 1;

( a < b? a : b) = 3; // 正确,返回a或b的引用,可做左值
( a < b? a : 1) = 3; // 错误,返回1或b的值,不可做左值

5、函数参数的默认值

    C++ 可以在函数声明时为参数提供一个默认值,当函数调用时没有提供参数的值,则使用默认值。

5.1 函数默认参数的规则

    函数参数的默认值在函数声明中指定,多使用这种方式

1
2
3
4
5
int add(int a = 0);
int add(int a)
{
return a;
}

    函数参数的默认值在函数定义中指定,很少或不建议使用

1
2
3
4
5
int add(int a);
int add(int a = 0)
{
return a;
}

    设计函数时,参数的默认值必须从右向左提供,函数调用时使用了默认值参数,则后续参数必须使用默认值参数调用函数时,从左向右匹配

1
2
3
4
5
6
7
8
int add( int x, int y = 1, int z = 2)
{
return x + y + z;
}

add(0); // x = 0, y = 1, z = 2
add(2,3); // x = 2, y = 3, z = 2
add(3,2,1); // x = 3, y = 2, z = 1

5.2 函数占位参数

    在 c++ 中可以为函数提供占位参数。占位参数只有参数类型声明,而没有参数申明。一般情况下,在函数体内部无法使用占位参数。

1
2
3
4
5
6
7
8
int func(int x , int)
{
return x;
}

// ...

func(1,2); // ok

    占位参数与默认参数结合起来使用,兼容 c 语言程序中可能出现的不规范写法

1
c: void fun()  <----> c++: void fun(int = 0)

6、函数重载(Over load)

    同一个标识符在不同的上下文有不同的意义,高级语言无限接近自然语言,重载来自于自然语言的动词+名词。

6.1 函数重载(Function Over load)

    用同一个函数名定义不同的函数,当函数名和不同的参数搭配时函数的含义不同。函数重载必须至少满足下面一个条件:参数的个数不同参数的类型不同参数的顺序不同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int func(int x)
{
return x;
}

int func(int a, int b)
{
return a + b;
}

int func(int a, const char* s)
{
return strlen(s);
}

int func(const char* s, int a)
{
return strlen(s);
}

    函数重载可能会与使用默认参数的函数冲突(c++特性冲出)

1
2
3
4
5
6
7
8
9
int func( int a, int b, int c = 5)
{
return a * b * c;
}

int func( int a, int b)
{
return a + b;
}

6.2 编译器调用重载函数的的规则

    将所用同名函数作为候选者,尝试寻找可行的后选函数。精确匹配实参通过默认参数能够匹配实参通过默认类型转换匹配实参。如果最终寻找到的候选函数不唯一,则出现二义性,则导致编译失败。无法匹配所有候选者,函数未定义,也会编译失败。

6.3 函数重载的注意事项

     重载函数在本质上是相互独立的不同函数。重载函数的函数类型不同。函数返回值不能作为函数重载的依据,函数重载是由函数名和参数列表决定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int add(int a, int b)        // int (int ,int)
{
return a + b;
}

int add(int a, int b,int c) // int (int ,int,int)
{
return a + b;
}

int main(int argc,char* argv[])
{
printf("%p\n",( int(*)(int ,int))add); // 0x8048494
printf("%p\n",( int(*)(int ,int,int))add); // 0x80484a2
return 0;
}

6.4 重载与指针

    将重载函数名赋值给指针时,根据重载规则挑选与函数指针参数列表一致的候选者。严格匹配候选者的函数类型与函数指针的函数类型

  • 函数重载必然发生在同一个作用域中
  • 编译器需要用参数列表或函数类型进行函数选择
  • 无法直接通过函数名得到重载函数的入口地址

7、c++ 和 c 相互调用

    实际工程中 c++ 和 c 代码相互调用是不可避免的。c++ 编译器能够兼容 c 语言编译方式。c++ 编译器优先使用 c++ 编译方式。extern 关键字强制让 c++ 编译器进行 c 方式编译。extern 代码块中不能存在重载函数。

1
2
3
4
extern “C”
{
// do c-style compilation here
}

保证一段c代码只会以c的方式被编译

__cplusplus是c++编译器内置的标准宏定义
__cplusplus的意义是确保c代码以统一的方式被编译成目标文件

1
2
3
4
5
6
7
8
9
10
#ifdef __cplusplus
extern "C" {
#endif

// C-Style Compilation


#ifdef __cplusplus
}
#endif
  • c++编译器不能以c的方式编译重载函数
  • 编译方式决定函数名被编译后的目标名
  • c++编译方式将函数名和参数列表编译成目标名
  • c编译方式只将函数名作为目标名进行编译

8、动态分配内存

8.1 c++中的动态分配内存

    c++ 中通过 new 关键字进行动态内存申请,c++ 中的动态内存申请是基于类型进行的,delete 关键字用于释放内存。

1
2
3
4
5
变量申请与释放:                         数组申请与释放:

Type* Pointer = new Type; Type* pointer = new Type[N];
//...... //......
delete pointer; delete[] pointer;

8.2 new 关键字与 mallloc 函数的区别

    new 关键字是 c++ 的一部分,malloc 是由 c 库提供的函数。new 以具体类型为单位进行内存分配malloc 以字节为单位进行内存分配new 在申请单个类型变量时可以进行初始化malloc 不具备内存初始化的特性。new 关键字的初始化如下:

1
2
3
int* p = new int(1);
float* f = new float(2.0f);
char* pc = new char(‘c’);

9、c++ 中的命名空间

    在 C 语言中只有一个全局作用域,C 语言中所有标识符共享一个作用域,标识符之间可能发生冲突。C++ 中提出了命名空间的概念:命名空间将全局作用域分成不同的部分& 命名空间可以相互嵌套全局作用域也叫默认命名空间无论怎么划分本质还是全局作用域

1
2
3
4
5
6
7
8
9
10
11
12

定义
namespace Name
{

namespace Internal
{
/* ... */
}

/* ... */
}
  • 使用整个命名空间:using namespace name;
  • 使用命名空间中的变量:using name::variable;
  • 使用默认命名空间中的变量:::variable;

10、新型的类型转换方式

10.1 C 语言中的强制类型转换

1
(type )( Expression )

    C 强制类型转换过于粗暴:任意类型之间都可以进行转换,编译器很难判断其正确性。难于定位:在源码中无法快速定位所有使用强制类型转换的语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef void(PF)(int);

sturcut Point
{
int x;
int y;
}

int v = 0x12345;
PF* pf = PF* (v);
char c = char (v);
Point* p = (Point*) v;

10.2 c++ 中的强制类型转换

    c++ 将强制类型转换分为 4 种不同的类型

10.2.1 static_cast 强制类型转换(静态类型转换)

    用于基本类型之间的转换,不能用于基本类型指针间的转换,用于有继承关系类对象之间的转换和类指针之间的转换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

void static_cast_demo()
{
int i = 0x12345;
char c = 'c';
int* pi = &i;
char* pc = &c;

c = static_cast<char>(i);
//pc = static_cast<char*>(pi); //ERROR 不能用于指针间的转换
}

int main()
{
static_cast_demo();

return 0;
}

10.2.2 const_cast 强制类型转换

    用于除去变量的只读属性,强制转换的目标类型必须是指针或引用

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
#include <stdio.h>

void const_cast_demo()
{
const int& j = 1;
int& k = const_cast<int&>(j);

const int x = 2;
int& y = const_cast<int&>(x);

//int z = const_cast<int>(x); //ERROR 目标类型必须是引用或者指针

k = 5;

printf("k = %d\n", k);
printf("j = %d\n", j);

y = 8;

printf("x = %d\n", x);
printf("y = %d\n", y);
printf("&x = %p\n", &x);
printf("&y = %p\n", &y);
}

int main()
{
const_cast_demo();

return 0;
}

10.2.3 reinterpret_cast 强制类型转换

    用于指针类型间的强制转换,用于整数和指针类型间的强制类型转换

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

void reinterpret_cast_demo()
{
int i = 0;
char c = 'c';
int* pi = &i;
char* pc = &c;

pc = reinterpret_cast<char*>(pi);
pi = reinterpret_cast<int*>(pc);
pi = reinterpret_cast<int*>(i);
pi = reinterpret_cast<int*>(66);
//c = reinterpret_cast<char>(i); // ERROR 指针与指针或整数和指针间的转换
}

int main()
{
reinterpret_cast_demo();
return 0;
}

10.2.4 dynamic_cast 强制类型转换

    用于有继承关系的类指针间的转换,用于有交叉关系的类指针间的转换,具有类型检查功能,需要虚函数的支持

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>

void dynamic_cast_demo()
{
int i = 0;
int* pi = &i;
char* pc = dynamic_cast<char*>(pi); //ERROR
}

int main()
{
dynamic_cast_demo();

return 0;
}

11、进阶面向对象

11.1 面向对象的基本概念

    面向对象的意义在于,将日常生活中习惯的思维方式引入程序设计中,将需求中的概念直观映射到解决方案中,以模块为中心构建可复用的软件系统,提高软件产品的可维护性

11.2 类和对象

    类: 指的一类的事物,是一个抽象的概念,类是一种模型,这种模型可以创建出不同的对象实体。对象: 指的是属于某个类的具体实体,对象实体是类模型的一个具体实例。

一个类可以有很多对象,而一个 对象必然属于某个类

11.3 类和对象的意义

    类用于描述一类事物所特有的属性和行为**,如:电脑拥有cpu,内存和硬盘并且可以开机和运行程序。对象是具体的事物**,拥有所属类中描述的一切行为和属性,如:每一只老虎都有不同的体重,不同的食量不同的性格

11.4 一些有趣的问题

类一定存在实际的对象吗?
不一定,恐龙对象不存在

类的对象数目是确定的吗?
不确定,老虎的数目不确定,皇帝类每一时期只有一个

类一定都是源于生活中吗?
不一定

类都是独立的吗?类之间存在关系吗?
不独立,类之间存在相互关系

对象实例一定只属于一个类吗?
不一定

对象实例可能完全相同吗?
不确定

11.5 类之间的基本关系

    继承: 从已存在的类细分出来的类和原类之间具有继承关系(is-a);继承的类(子类)拥有原类(父类的所有属性和行为)。

    组合: 一些类的存在必须依赖于其他的类,这种关系叫组合;组合的类在某一局部上由其他的类组成,将其他类的对象当做当前类的成员使用,当前类的对象与成员对象的生命周期相同,成员对象在用法上与普通对象完全一致

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 <stdio.h>

struct Biology { // 生物拥有生命
bool living;
};

struct Animal : Biology { // 动物继承生命的属性
bool movable;
void findFood() { }
};

struct Plant : Biology { // 植物继承生命的属性
bool growable;
};

struct Beast : Animal { // 野兽继承动物的属性和行为
void sleep() { }
};

struct Human : Animal { // 人类继承动物的属性和行为
void sleep() { }
void work() { }
};

int main()
{
return 0;
}

11.6 类与封装的概念

    类通常分为两个部分,类的实现细节,当创建类时,才需要考虑其内部实现细节;类的使用方式,当使用类时,不需要考虑其内部的实现细节。

    根据经验:并不是类的每个属性都是对外公开的;女孩不希望外人知道自己的体重和年龄;男孩子不希望外人知道自己的身高和收入。而一些类的属性是对外公开的;人的姓名,学历,国籍,等。因此必须在类的表示法中定义属性和行为的公开级别,类似于文件系统中的文件的权限。C++ 中类的封装如下。

  • 成员变量:C++ 中用于表示类属性的变量
  • 成员函数:C++ 中用于表示类行为的函数
  • Public:成员变量和成员函数可以在类的内部和外界访问调用
  • Private:成员变量和成员函数只能在类的内部被访问和调用
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
#include <stdio.h>

#include <stdio.h>

struct Biology
{
bool living;
};

struct Animal : Biology
{
bool movable;

void findFood()
{
}
};

struct Plant : Biology
{
bool growable;
};

struct Beast : Animal
{
void sleep()
{
}
};

struct Human : Animal
{
void sleep()
{
printf("I'm sleeping...\n");
}

void work()
{
printf("I'm working...\n");
}
};

struct Girl : Human
{
private:
int age;
int weight;
public:
void print()
{
age = 22;
weight = 48;

printf("I'm a girl, I'm %d years old.\n", age);
printf("My weight is %d kg.\n", weight);
}
};

struct Boy : Human
{
private:
int height;
int salary;
public:
int age;
int weight;

void print()
{
height = 175;
salary = 9000;

printf("I'm a boy, my height is %d cm.\n", height);
printf("My salary is %d RMB.\n", salary);
}
};

int main()
{
Girl g;
Boy b;

g.print();

b.age = 19;
b.weight = 120;
//b.height = 180; //ERROR 私有成员无法访问

b.print();

return 0;
}

11.7 类成员的作用域

    类成员的作用域都在类的内部,外部无法直接访问;成员函数可以直接访问成员变量和调用成员函数;类的外部可以通过类变量访问 public 成员类成员的作用域与访问级别没有关系(针对于外部访问);C++ 中 struct 定义的类中的所有成员默认为 public。

    想要访问类的成员变量或者成员函数都必须通过类的对象来进行,有了类的对像之后就可以访问类的成员变量和成员函数,是否访问成功还得看这个变量或者成员函数的访问级别。

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
#include <stdio.h>

int i = 1;

struct Test
{
private:
int i;

public:
int j;

int getI()
{
i = 3;

return i;
}
};

int main()
{
int i = 2;

Test test;

test.j = 4;

printf("i = %d\n", i); // i = 2;
printf("::i = %d\n", ::i); // ::i = 1;
// printf("test.i = %d\n", test.i); // Error
printf("test.j = %d\n", test.j); // test.j = 4
printf("test.getI() = %d\n", test.getI()); // test.getI() = 3

return 0;
}

12、类的真正形态

12.1 C++ 使用 class 关键字定义类

    在用 struct 定义类时,所有成员的默认访问级别为 public;在使用 class 定义类时,所有成员的默认访问级别为 private。

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
#include <stdio.h>

struct A
{
// defualt to public
int i;
// defualt to public
int getI()
{
return i;
}
};

class B
{
// defualt to private
int i;
// defualt to private
int getI()
{
return i;
}
};

int main()
{
A a;
B b;

a.i = 4;

printf("a.getI() = %d\n", a.getI());

b.i = 4; //ERROR

printf("b.getI() = %d\n", b.getI()); //ERROR

return 0;
}

    C++ 的类支持声明和实现的分离,将类的实现和定义分开;.h 头文件中只有类的声明,成员变量和成员函数的声明;.cpp 源文件中完成类的其他实现,成员函数的具体实现。

12.2 类实例

需求:开发一个用于四则运算的的类
提供setOperator函数设置运算类型
提供setParamater函数设置运算参数
提供result函数进行运算
      其返回值表示运算的合法性
      通过引用参数返回结果

  • 头文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#ifndef _OPERATOR_H_
#define _OPERATOR_H_

class Operator
{
private:
char mOp;
double mP1;
double mP2;

public:
bool setOperator(char op);
void setParameter(double p1, double p2);
bool result(double& r);
};

#endif
  • 类函数的具体实现
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
#include "Operator.h"

bool Operator::setOperator(char op)
{
bool ret = false;

if( (op == '+') || (op == '-') || (op == '*') || (op == '/') )
{
ret = true;
mOp = op;
}
else
{
mOp = '\0';
}

return ret;
}

void Operator::setParameter(double p1, double p2)
{
mP1 = p1;
mP2 = p2;
}

bool Operator::result(double& r)
{
bool ret = true;

switch( mOp )
{
case '/':
if( (-0.000000001 < mP2) && (mP2 < 0.000000001) )
{
ret = false;
}
else
{
r = mP1 / mP2;
}
break;
case '+':
r = mP1 + mP2;
break;
case '*':
r = mP1 * mP2;
break;
case '-':
r = mP1 - mP2;
break;
default:
ret = false;
break;
}

return ret;
}
  • 调用类的功能
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include "Operator.h"

int main()
{
Operator op;
double r = 0;

op.setOperator('/');
op.setParameter(9, 3);

if( op.result(r) )
{
printf("r = %lf\n", r);
}
else
{
printf("Calculate error!\n");
}

return 0;
}

13、对象的构造

13.1 对象的本质

    从程序设计的角度,对象只是变量,因此在上创建对象时,成员变量初始值为随机值、在上创建对象时,成员变量的初始值为随机值、在全局静态存储区创建对象时,成员变量初始值为 0。

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
#include "stdafx.h"
#include <stdio.h>

class Test
{
private:
int i;
int j;
public:
int getI(){return i;}
int getJ(){return j;}
};

Test gt; //全局对象

int main()
{
printf("gt.i = %d\n",gt.getI());
printf("gt.j = %d\n",gt.getJ());

Test t1; //局部对象

printf("t1.i = %d\n",t1.getI());
printf("t1.j = %d\n",t1.getJ());

Test *pt = new Test; // 堆空间里面的零时对象

printf("pt->i = %d\n",pt->getI());
printf("pt->j = %d\n",pt->getJ());

delete pt;

return 0;
}

编译结果:
gt.i = 0
gt.j = 0
t1.i = -858993460
t1.j = -858993460
pt->i = -842150451
pt->j = -842150451

13.2 构造函数

    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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include "stdafx.h"
#include <stdio.h>

class Test
{
private:
int i;
int j;
public:
int getI(){return i;}
int getJ(){return j;}
Test() //构造函数
{
i = 1;
j = 2;
}
};

Test gt; //全局对象

int main()
{
printf("gt.i = %d\n",gt.getI());
printf("gt.j = %d\n",gt.getJ());

Test t1; //局部对象

printf("t1.i = %d\n",t1.getI());
printf("t1.j = %d\n",t1.getJ());

Test *pt = new Test; //堆空间里面的零时对象

printf("pt->i = %d\n",pt->getI());
printf("pt->j = %d\n",pt->getJ());

delete pt;

return 0;
}

运行结果:
gt.i = 1
gt.j = 2
t1.i = 1
t1.j = 2
pt->i = 1
pt->j = 2

13.2.1 带有参数的构造函数

    构造函数可以根据需要定义参数,一个类可以存在多个重载的构造函数,构造函数的重载遵循 C++ 重载的规则。

1
2
3
4
5
6
7
8
class Test
{
public:
Test(int v)
{
// use v to initialize member
}
};

    对象定义和对象声明不同、对象定义-申请对象的空间并调用构造函数、对象声明-告诉编译器存在这样一个对象

1
2
3
4
5
6
7
8
9
Test t; // 定义对象并调用构造函数

int main()
{
// 告诉编译器存在名为 t 的 Test 对象
extern Test t;

return 0;
}
  • 编程实验: 构造函数的自动调用
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
#include "stdafx.h"
#include <stdio.h>

class Test
{
public:
Test()
{
printf("Test()\n");
}
Test(int v)
{
printf("Test(int v)\n");
};
};

int main()
{
Test t; //调用 Test()
Test t1(1); //调用 Test(int v)
Test t2 = 1; //调用 Test(int v)

return 0;
}

运行结果:
Test()
Test(int v)
Test(int v)

小贴士:
赋值与初始化是不同的

1
2
3
4
int i = 1;  //用 1 初始化 i
int j(100); //用 100 初始化 j

i = 1; //赋值

13.2.2 构造函数的调用

手工调用构造函数初始化对象

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
#include "stdafx.h"
#include <stdio.h>

class Test
{
private:
int m_value;
public:
Test()
{
printf("Test()\n");
m_value = 0;
}
Test(int v)
{
printf("Test(int v), v = %d\n",v);
m_value = v;
};
int getValue()
{
return m_value;
}
};

int main()
{
Test ta[3] = {Test(),Test(1),Test(2)}; // 手动调用构造函数初始化对象数组的成员变量

for(int i = 0; i<3; i++)
{
printf("ta[%d].m_value = %d\n",i,ta[i].getValue());
}

Test t = Test(3); //手动调用构造函数初始化对象

printf("t.m_value = %d\n",t.getValue());

return 0;
}

运行结果:
Test()
Test(int v), v = 1
Test(int v), v = 2
ta[0].m_value = 0
ta[1].m_value = 1
ta[2].m_value = 2
Test(int v), v = 3
t.m_value = 3

13.2.3 小实例

开发一个数组类解决原生数组的安全性问题
-提供函数获取数组长度
-提供函数获取数组元素
-提供函数设置数组元素

  • 头文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#ifndef _INTARRY_H_
#define _INTARRY_H_

class IntArry
{
private:
int m_length;
int* m_pointer;
public:
IntArry(int len);
int length();
bool get(int index, int& value);
bool set(int index, int value);
void free();
};

#endif
  • 函数功能实现
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
#include "StdAfx.h"
#include "intarry.h"

IntArry::IntArry(int len)
{
m_pointer = new int[len];

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

m_length = len;
}

int IntArry::length()
{
return m_length;
}

bool IntArry::get(int index, int& value)
{
bool ret = ( 0 <= index) && (index <= m_length);

if( ret )
{
value = m_pointer[index];
}

return ret;
}

bool IntArry::set(int index, int value)
{
bool ret = ( 0 <= index) && (index <= m_length);

if( ret )
{
m_pointer[index] = value;
}

return ret;
}


void IntArry::free()
{
delete[] m_pointer;
}
  • 主函数测试
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
#include "stdafx.h"
#include <stdio.h>
#include "intarry.h"

int main()
{

IntArry a(5);

for(int i=0; i<a.length(); i++)
{
a.set(i,i+1);
}

for(int i=0; i<a.length(); i++)
{
int value = 0;

if(a.get(i,value))
{
printf("a[%d] = %d\n",i,value);
}
}

a.free();

return 0;
}

运行结果:
a[0] = 1
a[1] = 2
a[2] = 3
a[3] = 4
a[4] = 5

13.3 特殊的构造函数

13.3.1 无参数构造函数

    没有参数的构造函数,当类中没有定义任意构造函数时,编译器默认提供一个无参造函数,并且其函数体为空。

13.3.2 拷贝构造函数

    参数为 const class_name& 的构造函数,当类中没有定义拷贝构造函数时,编译器默认提供一个拷贝构造函数,简单的进行成员变量的值复制(浅拷贝)。

拷贝构造函数是为了兼容C语言的初始化方式,初始化行为能够符合预期的逻辑。

13.3.2.1 浅拷贝

拷贝后对象的物理状态相同,编译器默认提供的拷贝构造函数只进行浅拷贝

  • 编程实验
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
#include "stdafx.h"
#include <stdio.h>
#include "intarry.h"

class Test
{
private:
int i;
int j;
int *p;
public:
int getI(){return i;}
int getJ(){return j;}
int* getP(){return p;}
Test(int v)
{
i = 1;
j = 2;
p = new int;
*p = v;
}

void free()
{
delete p;
}
};

int main()
{
Test t1(1);

Test t2 = t1;

printf("t1.i = %d, t1.j = %d,t1.p = %p\n",t1.getI(),t1.getJ(),t1.getP());
printf("t2.i = %d, t2.j = %d,t2.p = %p\n",t2.getI(),t2.getJ(),t2.getP());

t1.free();
//t2.free(); //多次释放造成内存泄漏

return 0;
}

运行结果:
t1.i = 1, t1.j = 2,t1.p = 01323488
t2.i = 1, t2.j = 2,t2.p = 01323488

运行结果分析,函数中没有拷贝构造函数因此,默认提供了一个拷贝构造函数如下

1
2
3
4
5
6
Test(const Test& t)
{
i = t.i;
j = t.j;
p = t.p;
}

    因此得到的指针 t1.p 与 t2.p 指向同一个内存空间,对象 t2 并没有新生成一个堆空间的 int 大小的内存。所以分别运行两个对象的 fre e释放函数会造成内存空间的重复释放,导致内存泄漏,编译报错。

13.3.2.2 深拷贝

拷贝后对象的逻辑状态相同

  • 编程实验

手动提供拷贝构造函数实现深拷贝

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
#include "stdafx.h"
#include <stdio.h>

class Test
{
private:
int i;
int j;
int *p;
public:
int getI(){return i;}
int getJ(){return j;}
int* getP(){return p;}

Test(int v)
{
i = 1;
j = 2;
p = new int;
*p = v;
}

Test(const Test& t) //手动提供构造函数
{
i = t.i;
j = t.j;
p = new int;
*p = *t.p;
}

void free()
{
delete p;
}
};

int main()
{
Test t1(1);

Test t2 = t1;

printf("t1.i = %d, t1.j = %d,t1.p = %p\n",t1.getI(),t1.getJ(),t1.getP());
printf("t2.i = %d, t2.j = %d,t2.p = %p\n",t2.getI(),t2.getJ(),t2.getP());

t1.free();
t2.free();

return 0;
}

运行结果:
t1.i = 1, t1.j = 2,t1.p = 00CF3488
t2.i = 1, t2.j = 2,t2.p = 00CF34C8

第24行手动提供拷贝构造函数实现深拷贝,通过28,29行操作实现深拷贝

13.3.3 什么时候需要深拷贝

  • 成员指向了动态内存空间
  • 成员打开了外存中的文件
  • 成员使用了系统中的网络端口

13.3.4 数组类的改进

头文件 intarry.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#ifndef _INTARRY_H_
#define _INTARRY_H_

class IntArry
{
private:
int m_length;
int* m_pointer;
public:
IntArry(int len);
IntArry(const IntArry& obj); //添加拷贝构造函数
int length();
bool get(int index, int& value);
bool set(int index, int value);
void free();
};

#endif

intarry.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
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
#include "StdAfx.h"
#include "intarry.h"

IntArry::IntArry(int len)
{
m_pointer = new int[len];

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

m_length = len;
}

IntArry::IntArry(const IntArry& obj) //手动实现深拷贝
{
m_pointer = new int[obj.m_length];

for(int i = 0; i < obj.m_length; i++)
{
m_pointer[i] = obj.m_pointer[i];
}

m_length = obj.m_length;
}

int IntArry::length()
{
return m_length;
}

bool IntArry::get(int index, int& value)
{
bool ret = ( 0<= index) && (index <= m_length);

if( ret )
{
value = m_pointer[index];
}

return ret;
}
bool IntArry::set(int index, int value)
{
bool ret = ( 0<= index) && (index <= m_length);

if( ret )
{
m_pointer[index] = value;
}

return ret;
}


void IntArry::free()
{
delete[] m_pointer;
}

测试函数

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
#include "stdafx.h"
#include <stdio.h>
#include "intarry.h"

int main()
{
IntArry a(5);

for(int i = 0; i< a.length(); i++)
{
a.set(i, i + 1);
}

for(int i = 0; i< a.length(); i++)
{
int value = 0;

if(a.get(i, value))
{
printf("a[%d] = %d\n",i,value);
}
}

printf("\n");

IntArry b = a;

for(int i = 0; i< b.length(); i++)
{
int value = 0;

if(b.get(i, value))
{
printf("b[%d] = %d\n",i,value);
}
}

a.free();
b.free();

return 0;
}

14、初始化列表

用于初始化类对象中的成员变量

1
2
3
4
5
ClassName::ClassName():
m1(v1), m2(v1,v2), m3(v3)
{
// some other initialize opreration
}

14.1 初始化列表的初始顺序

    成员的初始化顺序与成员的申明顺序相同,成员的初始化顺序与初始化列表中的位置无关,初始化列表先于构造函数的函数体执行。编程实验:验证初始化列表的初始顺序

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
#include "stdafx.h"
#include <stdio.h>

class Value
{
private:
int mi;
public:
Value(int i)
{
printf("i = %d\n",i);
mi = i;
}

int getMI()
{
return mi;
}
};

class Test
{
private:
Value m2; // 初始化列表先初始化这个变量
Value m3;
Value m1; // 初始化列表最后初始化这个变量
public:
Test() : m1(1),m2(2),m3(3)
{
printf("Test\n");
}

const int getCI()
{
return ci;
}
};

int main()
{
Test t;

return 0;
}

运行结果:
i = 2
i = 3
i = 1
Test

14.2 类中的 const 成员

    类中的 const 成员会被分配内存空间、类中的 const 成员的本质是只读变量、类中的 const 成员只能在初始化列表中指定初始化值、编译器无法直接得到 const 成员的初始值,因此无法进入符号表成为真正意义上的常量。编程实验:类中的 const 成员变量本质为只读变量

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
#include "stdafx.h"
#include <stdio.h>
#include "intarry.h"

class Test
{
private:
const int ci;
public:
Test() : ci(0)
{
}

const int getCI()
{
return ci;
}

void setCI(int v)
{
int *p = const_cast<int *>(&ci); //通过指针修改ci的值

*p = v;
}
};

int main()
{
Test t;

printf("t.ci = %d\n",t.getCI());

t.setCI(2);

printf("t.ci = %d\n",t.getCI());

return 0;
}

运行结果:
t.ci = 0
t.ci = 2

初始化与赋值不同
初始化:对正在创建的对象进行初始值设置
赋值:对已经存在的对象进行值的设置

15、对象的构造顺序

1. 局部对象
当程序执行流到达对象定义的语句时进行构造

2. 堆对象的构造顺序
堆对象的构造与 new 关键字以及程序执行流有关

3. 全局对象的构造顺序
对象的构造顺序是不确定的,不同的编译器使用不同规则的构造顺序

尽量避免使用全局变量与全局对象,以及全局对象之间的依赖

16、对象的销毁

    C++ 类中定义的一个特殊的清理函数,析构函数的功能与构造函数相反

1
~ClassName()

-析构函数没有参数也没有返回值类型声明
-析构函数在对象销毁值自动被调用

编程实验:验证析构函数的自动调用

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
#include "stdafx.h"
#include <stdio.h>

class Test
{
public:
Test()
{
printf("Test()\n");
}
~Test()
{
printf("~Test()\n");
}
};

int main()
{

{ //定义对象的作用域
Test t; // 对象被销毁后自动调用析构函数
}

return 0;
}

运行结果:
Test()
~Test()

    析构函数的定义准则: 当类中定义了构造函数,并且构造函数使用了系统资源(内存申请,文件打开,等)则需要自定义析构函数,析构函数是对象释放系统资源的保障

17、临时对象

17.1 临时对象的产生

    直接调用构造函数将产生一个临时对象、临时对象的生命周期只有一条语句的时间、临时对象的作用于只在一条语句中、临时对象时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
34
35
#include "stdafx.h"
#include <stdio.h>

class Test
{
private:
int mi;
public:
Test(int i)
{
mi = i;
}

Test()
{
Test(1);//直接调用构造函数产生零时对象
}

void print()
{
printf("mi = %d\n",mi);
}
};

int main()
{

Test t;
t.print();

return 0;
}

运行结果:
mi = -858993460

    运行结果分析:本来第 16 行调用构造函数的目的是初始化 mi,使得 mi 的值为1,但运行结果为一个随机值,产生的原因就是 16 行直接调用有参数的构造函数得到了一个临时对象。16 行的临时对象的生命周期只有 16 行这条语句,过了这条语句之后这个临时对象就被析构。这个临时对象同时没有名字,所以临时对象的作用域也只有这一条语句。因此16行这条语句没有半毛钱作用。16行代码等价于一条空语句。

解决方案,提供一个私有的初始化函数。

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 "stdafx.h"
#include <stdio.h>

class Test
{
private:
int mi;
void init(int i) // 提供私有的init函数
{
mi = 1;
}
public:
Test(int i)
{
mi = i;
}
Test()
{
init(1); // 调用私有的init初始化函数
}

void print()
{
printf("mi = %d\n",mi);
}
};

int main()
{
Test t;
t.print();

return 0;
}

运行结果:
mi = 1

深入验证临时对象

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
#include "stdafx.h"
#include <stdio.h>

class Test
{
private:
int mi;
void init(int i)
{
mi = 1;
}
public:
Test(int i)
{
printf("Test(int i)\n");
mi = i;
}
Test()
{
printf("Test()\n");
init(1);
}
~Test()
{
printf("~Test()\n");
}
void print()
{
printf("mi = %d\n",mi);
}
};

int main()
{
printf("Main begin \n");

Test().print(); //直接调用构造函数产生临时对象
printf("\n");
Test(3).print(); //直接调用构造函数产生临时对象

printf("Main end... \n");

return 0;
}
运行结果:
Main begin
Test()
mi = 1
~Test()

Test(int i)
mi = 3
~Test()
Main end...

    运行结果分析:37 行与 39 行直接调用构造函数产生临时对象,因此 Test()/Test(3) 为临时生成的对象,可以直接调用他们的成员函数 print。同时也验证了临时对象的生命周期为一条语句的时间。

17.2 临时对象的优化

    现代 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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include "stdafx.h"
#include <stdio.h>

class Test
{
private:
int mi;
void init(int i)
{
mi = 1;
}
public:
Test(int i)
{
printf("Test(int i)\n");
mi = i;
}
Test()
{
printf("Test()\n");
init(1);
}
~Test()
{
printf("~Test()\n");
}
void print()
{
printf("mi = %d\n",mi);
}
Test(const Test& obj)
{
mi = obj.mi;
printf("Test(const Test& obj): %d\n",mi);
}
};

Test func()
{
return Test(3);
}

int main()
{
printf("Main begin \n");

Test t = Test(1); // ==> Test t = {1};

Test t3 = func(); // ==> Test t3 = Test(3) ==> Test t3 = {3};

printf("Main end... \n");

return 0;
}

第47,49行运行个过程应该如下

  1. 生成临时对象
  2. 用临时对象初始化t对象
  3. 调用拷贝构造函数

但实际的运行结果发现并没有拷贝构造函数的调用,产生这个结果的原因就是编译器为了杜绝临时对象的产生因此对代47,49行码进行了优化,优化后的代码如下

1
2
Test t = {1};
Test t3 = {3};

所以代码没有调用拷贝构造函数

注意: 临时对象是性能的瓶颈,也是 bug 的来源之一,实际工程开发中需要人为的避开临时对象

18、经典问题分析

18.1 构造函数与析构函数的调用顺序

18.1.1 单个对象创建时

  1. 调用父类的构造过程
  2. 调用成员变量的构造函数(调用顺序与声明顺序相同)
  3. 调用自身类的构造函数

析构函数与对应构造函数的调用顺序相反

编程实验 :

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
#include "stdafx.h"
#include <stdio.h>

class Member
{
private:
const char* ms;
public:
Member(const char* s)
{
printf("Member(const char* s): %s\n",s);
ms = s;
}

~Member()
{
printf("~Member(): %s\n",ms);
}
};

class Test
{
private:
Member mA;
Member mB;
public:
Test(): mB("mB"), mA("mA")
{
printf("Test()\n");
}

~Test()
{
printf("~Test()\n");
}

};

int main()
{
{
Test t;
}
return 0;
}

运行结果:
Member(const char* s): mA
Member(const char* s): mB
Test()
~Test()
~Member(): mB
~Member(): mA

18.1.2 栈对象和全局对象

    对于栈对象和全局对象,类似于入栈于出栈的顺序,最后构造的对象被最先析构

18.1.3 堆对象

    堆对象的构造发生在 new 关键字调用的时候,析构则是发生在使用 delete 的时候,与 delete 的使用顺序有关。

18.2 const 对象

    const 能够修饰对象,const 修饰的对象为只读对象,只读对象的成员变量不允许改变只读对象是编译阶段的概念,运行期无效

    const 对象只能调用 const 的成员函数,const 成员函数中只能调用 const 成员函数,const 成员函数不能直接改写成员变量的值。const成员函数的定义如下

Type ClassName::function(type p) const

类中的声明和实际的成员函数中都必须带 const 关键字

18.3 对象的构成

    从面向对象的角度:对象由属性(成员变量),和方法(成员函数)构成。从程序运行角度: 对象由数据函数组成,数据可以位于栈,堆和全局数据区、函数只能位于代码段

    每一个对象拥有自己独立的属性(成员变量)。所有的对象共享创建他的的类的方法(成员函数)。方法能够直接访问对象的属性。方法中的隐藏参数 this 用于指代当前对象。成员函数只有一套,他能直接访问任何所属类对象的成员变量。

编程实验:

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
#include "stdafx.h"
#include <stdio.h>

class Test
{
private:
int mi;
public:
Test(int i);
Test(const Test& obj);
int getMi() const;
void print();

};

Test::Test(int i)
{
mi = i;
}

Test::Test(const Test& obj)
{
mi = obj.mi;
}

int Test::getMi()const
{
return mi;
}

void Test::print()
{
printf("this = %p\n\n",this);
}

int main()
{
Test t1(1);
Test t2(2);
Test t3(3);

printf("t1.getMi() = %d\n",t1.getMi());
printf("&t1 = %p\n",&t1);
t1.print();

printf("t2.getMi() = %d\n",t2.getMi());
printf("&t2 = %p\n",&t2);
t2.print();

printf("t3.getMi() = %d\n",t3.getMi());
printf("&t3 = %p\n",&t3);
t3.print();

return 0;
}

运行结果:
t1.getMi() = 1
&t1 = 0x7ffee9d6ff9c
this = 0x7ffee9d6ff9c

t2.getMi() = 2
&t2 = 0x7ffee9d6ffa0
this = 0x7ffee9d6ffa0

t3.getMi() = 3
&t3 = 0x7ffee9d6ffa4
this = 0x7ffee9d6ffa4

19、 类的静态成员变量与成员函数

19.1 类的静态成员变量

    静态成员变量属于整个类所有所有对象共享类的静态成员变量,静态成员变量的生命周期不依赖与任何对象,可以通过对象名访问公有静态成员变量,可以通过类名直接访问公有静态成员变量,定义静态成员变量的语法规则如下:

1
Type ClassName::VarName = value;

    在定义时直接通过 static 关键字修饰,静态成员变量需要在类外单独分配空间,静态成员变量在程序内部位于全局数据区

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
#include <stdio.h>

class Test
{
private:
static int cCount; // 静态成员变量
public:
Test()
{
cCount++;
}
~Test()
{
--cCount;
}
int getCount()
{
return cCount;
}
};

int Test::cCount = 0; // 通过类名直接访问静态成员变量

Test gTest; // 调用构造函数 cCount = 1

int main()
{
Test t1; // 调用构造函数 cCount = 2
Test t2; // 调用构造函数 cCount = 3

printf("count = %d\n", gTest.getCount()); // count = 3
printf("count = %d\n", t1.getCount()); // count = 3
printf("count = %d\n", t2.getCount()); // count = 3

Test* pt = new Test(); // 调用构造函数 cCount = 4

printf("count = %d\n", pt->getCount()); // count = 4

delete pt; // 调用析构函数 cCount = 3

printf("count = %d\n", gTest.getCount()); // count = 3

return 0;
}

19.2 类的静态成员函数

    静态成员函数是类中特殊的成员函数,静态成员函数属于整个类所有,可以通过类名直接访问共有静态成员函数,可以通过对象名访问公有静态成员函数。

19.2.1 静态成员函数的定义

    直接通过 static 关键字修饰生源函数

1
2
3
4
5
6
7
8
9
10
11
class Test
{
public:
static void func1(){};
static int func2();
}

int Test::func2()
{
return 0;
}

编程体验:

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
#include "stdafx.h"
#include <stdio.h>

class Demo
{
private:
int i;
public:
int getI();
static void staticFunc(const char* s);
static void staticSetI(Demo& d,int v);
};

int Demo::getI()
{
return i;
}

void Demo::staticFunc(const char* s)
{
printf("staticFunc: %s\n",s);
}


void Demo::staticSetI(Demo& d,int v)
{
d.i = v;
}

int main()
{

Demo::staticFunc("main begin..."); //直接通过类名调用静态成员函数

Demo d;
d.staticSetI(d,1); //通过对象名访问静态成员函数
printf("d.i = %d\n",d.getI());

Demo::staticFunc("main end...");
return 0;
}

运行结果:
staticFunc: main begin...
d.i = 1
staticFunc: main end...

19.2.1 普通与静态成员函数对比

静态成员函数vs普通成员函数 静态成员函数 普通成员函数
所有对象共享 Yes Yes
隐含this指针(函数) No Yes
访问普通成员变量(函数) No Yes
通过类名直接调用 Yes No
通过对象名直接调用 Yes Yes

19.3 小实例

    需求:统计在程序运行期间某个类的对象数目,保证程序的安全性(不能使用全局变量),随时可以获取当前对象的数目

编程实现

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 "stdafx.h"
#include <stdio.h>

class Test
{
private:
static int mCount;
public:
Test()
{
mCount++;
}
~Test()
{
mCount--;
}
static int getCount()
{
return mCount;
}
};

int Test::mCount = 0; //定义静态成员变量,并初始化为0

int main()
{
Test t0;
Test t1;
Test t2;

printf("Test::mCount = %d\n",Test::getCount()); //通过类名访问静态函数得到mCount

Test* pt = new Test;

printf("Test::mCount = %d\n",Test::getCount());

delete pt;

printf("Test::mCount = %d\n",Test::getCount());

return 0;
}

运行结果:
Test::mCount = 3
Test::mCount = 4
Test::mCount = 3

20、二阶构造模式

    构造函数只能提供自动初始化成员变量的机会,不能保证初始化逻辑一定成功,执行 return 语句后构造函数立即结束。构造函数能决定的只是对象的初始化状态,而不是对象的诞生。

20.1 半成品对象

    初始化操作不能按照预期完成而得到的对象,半成品对象是合法的 C++ 对象也是 Bug 重要来源

20.2 划分构造过程

    资源无关的初始化操作,不可能出现异常情况的操作。需要使用系统资源的操作,可能出现异常情况,如:内存申请,访问文件。二阶构造能够确保创建的对象都是完整初始化的。

–插图

20.3 二阶构造示例

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
#include "stdafx.h"
#include <stdio.h>

class TwoPhaseCons
{
private:
TwoPhaseCons(){ //第一阶段构造函数

}

bool construct(){ //第二阶段构造函数
return true;
}
public:
static TwoPhaseCons* NewInstance(); //对象创建函数
};

TwoPhaseCons* TwoPhaseCons::NewInstance()
{
TwoPhaseCons* ret = new TwoPhaseCons;

// 若二阶构造失败,返回NULL
if(!(ret && ret->construct()))
{
delete ret;
ret = NULL;
}

return ret;
}

int main()
{
TwoPhaseCons* p = TwoPhaseCons::NewInstance();
TwoPhaseCons* p1 = TwoPhaseCons::NewInstance();

printf("p = %p\n",p);
printf("p1 = %p\n",p1);

return 0;
}

20.4 优化数组类的实现

intarry.h文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#ifndef _INTARRY_H_
#define _INTARRY_H_

class IntArry
{
private:
int m_length;
int* m_pointer;
IntArry(int len);
bool construct();
public:
static IntArry* NewIntArry(int len); //对象的创建函数
IntArry(const IntArry& obj);
int length();
bool get(int index, int& value);
bool set(int index, int value);
void free();
};

intarry.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
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
#include "StdAfx.h"
#include "intarry.h"

IntArry::IntArry(int len) //一阶构造函数
{
m_length = len;
}

bool IntArry::construct() //二阶构造函数
{
bool ret = true;

m_pointer = new int[m_length];

if( m_pointer )
{
for(int i = 0; i < m_length; i++)
{
m_pointer[i] = 0;
}
}
else
{
ret = false;
}

return ret;
}

IntArry::IntArry(const IntArry& obj)
{
m_pointer = new int[obj.m_length];

for(int i = 0; i < obj.m_length; i++)
{
m_pointer[i] = obj.m_pointer[i];
}

m_length = obj.m_length;
}

IntArry* IntArry::NewIntArry(int len)
{
IntArry* ret = new IntArry(len);

if(!(ret && ret->construct()))
{
delete[] ret;
ret = NULL;
}

return ret;
}

int IntArry::length()
{
return m_length;
}

bool IntArry::get(int index, int& value)
{
bool ret = ( 0<= index) && (index <= m_length);

if( ret )
{
value = m_pointer[index];
}

return ret;
}
bool IntArry::set(int index, int value)
{
bool ret = ( 0<= index) && (index <= m_length);

if( ret )
{
m_pointer[index] = value;
}

return ret;
}


void IntArry::free()
{
delete[]m_pointer;
}

测试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
#include "stdafx.h"
#include <stdio.h>
#include "intarry.h"

int main()
{
IntArry* a = IntArry::NewIntArry(5);

if(a)
{
a->set(0,1);

for(int i = 0; i<a->length(); i++)
{
int value = 0;
a->get(i,value);
printf("a[%d] = %d\n",i,value);
}
}

delete a;

return 0;
}

运行结果:
a[0] = 1
a[1] = 0
a[2] = 0
a[3] = 0
a[4] = 0

21、被遗弃的友元

    友元是 C++ 中的一种关系,友元关系发生在函数与类之间或者类与类之间,友元关系是单项的,不能传递。在类中以 friend 关键字声明友元,类的友元可以是其他类或者具体函数,友元不是类的一部分,友元不受类中访问级别的限制,友元可以直接访问具体类的所有成员。在类中用 friend 关键字对函数或类进行声明。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Piont
{
double x;
double y;

friend void func(Point& p); // 声明了友元关系
}

void func(Point& p)
{
p.x = 1; // 可以直接访问类的私有成员变量
p.y = 2; // 可以直接访问类的私有成员变量
}

21.1 友元的尴尬

    友元是为了兼顾 C 语言的高效而诞生的,友元直接破坏了面向对象的封装特性,友元在实际产品中的高效是得不偿失的,友元在现代软件工程中已经逐渐被遗弃(对比 c 语言的 goto 语句)

注意事项:
友元不具备传递性
类的友元可以是其他类的成员函数
类的友元可以是某个完整的类:所有的成员函数都是友元

插图

编程实验:

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
#include "stdafx.h"
#include <stdio.h>

class ClassC
{
private:
const char* n;
public:
ClassC(const char* n)
{
this->n = n;
}

friend class ClassB;

};

class ClassB
{
private:
const char* n;
public:
ClassB(const char* n)
{
this->n = n;
}

void getClassCName(ClassC& c)
{
printf("c.n = %s\n",c.n);
}

friend class ClassA;
};

class ClassA
{
private:
const char* n;
public:
ClassA(const char* n)
{
this->n = n;
}

void getClassBName(ClassB& b)
{
printf("b.n = %s\n",b.n);
}


/* //友元没有传递性,无法直接访问 c 的成员函数
void getClassCName(ClassC& c)
{
//友元没有传递性,无法直接访问 c 的成员变量
printf(c.n = %s\n",c.n);
}
*/

};


int main()
{
ClassC c("C");
ClassB b("B");
ClassA a("A");

b.getClassCName(c);
a.getClassBName(b);
//a.getClassCName(c);

return 0;
}

22、类中的重载

    类中成员函数可以进行重载,构造函数的重载,普通成员函数的重载,静态成员函数的重载。重载函数的本质为多个不同的函数函数名和参数列表是唯一表示,函数重载必须发生在同一个作用域中

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
#include <stdio.h>

class Test
{
int i;
public:
Test()
{
printf("Test::Test()\n");
this->i = 0;
}

Test(int i)
{
printf("Test::Test(int i)\n");
this->i = i;
}

Test(const Test& obj)
{
printf("Test(const Test& obj)\n");
this->i = obj.i;
}

static void func()
{
printf("void Test::func()\n");
}

void func(int i)
{
printf("void Test::func(int i), i = %d\n", i);
}

int getI()
{
return i;
}
};

void func()
{
printf("void func()\n");
}

void func(int i)
{
printf("void func(int i), i = %d\n", i);
}

int main()
{
func();
func(1);

Test t; // Test::Test()
Test t1(1); // Test::Test(int i)
Test t2(t1); // Test(const Test& obj)

func(); // void func()
Test::func(); // void Test::func()

func(2); // void func(int i), i = 2;
t1.func(2); // void Test::func(int i), i = 2
t1.func(); // void Test::func()

return 0;
}

    通过函数名对函数功能进行提示,通过参数列表对函数用法进行提示,扩展系统中已经存在的函数功能。

23、操作符重载

    通过 operator 关键字可以定义特殊的函数,operator 的本质是通过函数重载操作符

1
2
3
4
5
6
7
8
Type operatorSign(const Type p1, const Type p2)
{
Type ret;

return ret;
}

sign: 为系统中预定义的操作符,如:+,-,*,/,等

编程实验:

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
#include "stdafx.h"
#include <stdio.h>

class Complex
{
private:
int a;
int b;
public:
Complex(const int a, const int b)
{
this->a = a;
this->b = b;
}

int getA()
{
return a;
}

int getB()
{
return b;
}

friend Complex operator+(const Complex& p1, const Complex& p2);
};

Complex operator+(const Complex& p1, const Complex& p2)
{
Complex ret(0,0);

ret.a = p1.a + p2.a;
ret.b = p1.b + p2.b;

return ret;
}

int main()
{
Complex a(1,2);
Complex b(1,2);

Complex c = a + b; // operator+(a,b)

printf("c.a = %d, c.b = %d\n",c.getA(),c.getB());

return 0;
}

运行结果:
c.a = 2, c.b = 4

    将操作符重载函数定义为类的成员函数、比全局操作符重载函数少一个参数(左操作数)、不需要依赖友元就可以完成操作符重载、编译器有限在成员函数中寻找操作符重载函数。

1
2
3
4
5
6
7
8
9
10
class Type
{
public:
Type operator Sign(const Type& p)
{
Type ret;

return ret;
}
}

编程实验:

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
#include <stdio.h>

class Test
{
private:
int a;
int b;
public:
Test();
Test(int a, int b);
Test operator+(const Test& p);
int getA(void);
int getB(void);
};


Test::Test()
{
a = 0;
b = 0;
}

Test::Test(int a, int b)
{
this->a = a;
this->b = b;
}

Test Test::operator +(const Test& p)
{
Test ret;

ret.a = this->a + p.a;
ret.b = this->b + p.b;

return ret;
}

int Test::getA(void)
{
return a;
}

int Test::getB(void)
{
return b;
}

int main()
{
Test t1 ={1,2};
Test t2 ={1,2};

Test t3 = t1 + t2;

printf("t3.a = %d, t3.b = %d\n",t3.getA(), t3.getB());

return 0;
}

运行结果:
t3.a = 2, t3.b = 4

23.1 实现一个复数类

复数类应该有的操作
-运算:+,-,*,/
-比较:==,!=
-赋值:=
-求模:modulus

编程实现:利用操作符重载,统一复数与实数的运算方式,统一复数与实数的比较方式

complex.h文件

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
#ifndef _COMPLEX_H_
#define _COMPLEX_H_

class Complex
{
private:
double a;
double b;
public:
Complex(double a = 0, double b = 0);
double getA();
double getB();
double getModulus();

Complex operator + (Complex& c);
Complex operator - (Complex& c);
Complex operator * (Complex& c);
Complex operator / (Complex& c);

bool operator == (Complex& c);
bool operator != (Complex& c);

Complex& operator = (const Complex& c);
};

#endif

complex.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
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
#include "StdAfx.h"
#include "complex.h"
#include <math.h>

Complex::Complex(double a, double b)
{
this->a = a;
this->b = b;
}

double Complex::getA()
{
return a;
}

double Complex::getB()
{
return b;
}

double Complex::getModulus()
{
return sqrt(a * a + b * b);
}


Complex Complex::operator + (Complex& c)
{
double na = a + c.a;
double nb = b + c.b;
Complex ret(na,nb);

return ret;
}

Complex Complex::operator - (Complex& c)
{
double na = a - c.a;
double nb = b - c.b;
Complex ret(na,nb);

return ret;
}

Complex Complex::operator * (Complex& c)
{
double na = a * c.a - b * c.b;
double nb = a * c.b + b * c.a;
Complex ret(na,nb);

return ret;
}

Complex Complex::operator / (Complex& c)
{
double cm = c.a*c.a + c.b+c.b;
double na = (a * c.a - b * c.b) / cm;
double nb = (b * c.a - a * c.b) / cm;
Complex ret(na,nb);

return ret;
}

bool Complex::operator == (Complex& c)
{
return (a==c.a) && (b==c.b);
}

bool Complex::operator != (Complex& c)
{
return !(*this == c);
}

Complex& Complex::operator = (const Complex& c)
{
if(this != &c)
{
a = c.a;
b = c.b;
}

return *this;
}

测试主程序:

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
#include "stdafx.h"
#include <stdio.h>
#include "complex.h"

int main()
{
Complex c1(1,2);
Complex c2(3,6);

Complex c3 =c1 + c2;
Complex c4 =c1 - c2;
Complex c5 =c1 * c2;
Complex c6 =c1 / c2;

printf("c3.a = %f, c3.b = %f\n",c3.getA(),c3.getB());
printf("c4.a = %f, c4.b = %f\n",c4.getA(),c4.getB());
printf("c5.a = %f, c5.b = %f\n",c5.getA(),c5.getB());
printf("c6.a = %f, c6.b = %f\n",c6.getA(),c6.getB());

Complex c7(1,-4);

printf("c4 == c7: %d\n",c4 == c7);
printf("c4 != c7: %d\n",c4 != c7);

return 0;
}

运行结果:
c3.a = 4.000000, c3.b = 8.000000
c4.a = -2.000000, c4.b = -4.000000
c5.a = -9.000000, c5.b = 12.000000
c6.a = -0.428571, c6.b = 0.000000
c4 == c7: 0
c4 != c7: 1

注意事项:
C++ 规定赋值操作符(=)只能重载为成员函数
操作符重载不能改变原操作符的优先级
操作符重载不能改变操作数的个数
操作符重载不应改变操作符的原有语义

有趣的重载:重载左移操作符,将变量或常量左移到一个对象中!

手动通过重载简单实现cout:

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
#include "stdafx.h"
#include <stdio.h>

const char endl = '\n';

class Console
{
public:
Console& operator << (int i)
{
printf("%d",i);
return *this;
}
Console& operator << (char c)
{
printf("%c",c);
return *this;
}
Console& operator << (char* c)
{
printf("%s",c);
return *this;
}
Console& operator << (double f)
{
printf("%f",f);
return *this;
}
};


Console cout;

int main()
{
cout << 1;
cout << endl << "123\n" << 1.25 << endl;

return 0;
}

24、 C++ 标准库

    C++ 标准库并不是 C++ 语言的一部分、C++ 标准库是由类库和函数库组成的集合、C++ 标准库中定义的<>类和对象都位于 std 命名空间中、C++ 标准库的头文件都不带 .h 后缀、C++ 标准库涵盖了 C 库的功能。C++ 编译环境的组成如下:

插图

C++标准库预定义了多数常用的数据结构

bitset set cstdio

编程体验 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
#include "stdafx.h"
#include <iostream>

using namespace std;

int main()
{

cout << "Hello Word!" << endl;

double a;
double b;

cout << "input a: ";
cin >> a;

cout << "input b: ";
cin >> b;

double c = sqrt(a*a + b*b);

cout << "c = " << c << endl;
return 0;
}

25、C++ 中的字符串类

    C++ 语言直接支持 C 语言所有的概念,C++ 语言中没有原生的字符串类型,C++ 标准库提供了string 类型,string 直接支持字符串连接,string 直接支持字符串的大小比较,string 直接支持子串查找和提取,string 直接支持字符串的插入和替换

25.1 字符串与数字的转换

标准库中提供了相关类对字符串和数字的转换,字符串流类(sstream)用于string的转换

  • <sstream>     - 相关头文件
  • istringstream  - 字符串输入流
  • ostringstream - 字符串输出流

使用方法
string->数字

1
2
3
istringstream iss("123.45");
double num;
iss >> num;

数字->string

1
2
3
ostringstream oss;
oss << 562.63;
string s = oss.str();

编程实现:字符串转换为数字与数字转换为字符串的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include "stdafx.h"
#include <iostream>
#include <string>
#include <sstream>

using namespace std;

#define TO_NUMBER(str,n) (istringstream(str) >> n)
#define TO_STRING(n) (((ostringstream&)(ostringstream() << n)).str())

int main()
{
double n;
TO_NUMBER("1.23",n);
cout << n << endl;

cout << TO_STRING(12.34) << endl;

return 0;
}

运行结果:
1.23
12.34

字符串循环右移

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
#include "stdafx.h"
#include <iostream>
#include <string>
#include <sstream>

using namespace std;

string right_func(const string& s, unsigned int n)
{
string ret = "";

unsigned int pos;

n = n % s.length();
pos = s.length() - n;

ret = s.substr(pos);
ret += s.substr(0,pos);

return ret;
}

int main()
{
string r = right_func("123456",8); //循环右移8位
cout << r << endl;

return 0;
}

运行结果:
561234

25.2 数组操作符的重载

    string 类最大限度的考虑了 c 字符串的兼容性,可以按照使用 c 字符串方式使用 string 类对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include <string>

using namespace std;


int main()
{
string str = "123abc456";

for(int i = 0; i < str.length(); i++)
{
cout << str[i] << endl;
}

return 0;
}

    数组访问符是 C/C++ 中的内置操作符,数组访问符的原生意义是数组访问和指针运算。

1
a[n] <--> *(a + n) <--> *(n + a) <--> n[a]

    数组访问操作符只能通过类的成员函数重载,重载函数能且仅能使用一个参数,可以定义多个不同参数的多个重载函数。优化数组类

interry.h文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#ifndef _INTARRY_H_
#define _INTARRY_H_

class IntArry
{
private:
int m_length;
int* m_pointer;
IntArry(int len);
bool construct();
public:
static IntArry* NewIntArry(int len);
IntArry(const IntArry& obj);
~IntArry();
int length();
bool get(int index, int& value);
bool set(int index, int value);
void free();
IntArry& self();
int& operator [] (int n);
};

#endif

interry.cpp文件:

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
#include "StdAfx.h"
#include "intarry.h"

IntArry::IntArry(int len)
{
m_length = len;
}

bool IntArry::construct()
{
bool ret = true;

m_pointer = new int[m_length];

if( m_pointer )
{
for(int i = 0; i < m_length; i++)
{
m_pointer[i] = 0;
}
}
else
{
ret = false;
}

return ret;
}

IntArry::IntArry(const IntArry& obj)
{
m_pointer = new int[obj.m_length];

for(int i = 0; i < obj.m_length; i++)
{
m_pointer[i] = obj.m_pointer[i];
}

m_length = obj.m_length;
}

IntArry* IntArry::NewIntArry(int len)
{
IntArry* ret = new IntArry(len);

if(!(ret && ret->construct()))
{
delete[] ret;
ret = NULL;
}

return ret;
}

int IntArry::length()
{
return m_length;
}

bool IntArry::get(int index, int& value)
{
bool ret = ( 0<= index) && (index <= m_length);

if( ret )
{
value = m_pointer[index];
}

return ret;
}
bool IntArry::set(int index, int value)
{
bool ret = ( 0<= index) && (index <= m_length);

if( ret )
{
m_pointer[index] = value;
}

return ret;
}

IntArry::~IntArry()
{
delete[]m_pointer;
}

int& IntArry::operator [] (int n)
{
return m_pointer[n];
}

IntArry& IntArry::self()
{
return *this;
}

测试主函数:

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
#include "stdafx.h"
#include <iostream>
#include "intarry.h"

using namespace std;

int main()
{
IntArry* a = IntArry::NewIntArry(5);

if(a != NULL)
{
IntArry& arry = a->self();

arry[0] = 1;

for(int i=0; i<arry.length(); i++)
{
cout << arry[i] << endl;
}
}

return 0;
}

运行结果:
1
0
0
0
0

27、经典问题解析

27.1 关于赋值的疑问

    编译器为每个类默认重载了赋值操作符,默认的赋值操作符仅完成浅拷贝,当需要进行深拷贝时必须重载赋值操作符,赋值操作符与拷贝构造函数有相同的存在意义。重载赋值操作符,必然需要实现深拷贝

编译器默认提供的函数

1
2
3
4
class Test
{

}

上述函数体等价于

1
2
3
4
5
6
7
8
class Test
{
public:
Test();
Test(const Test& );
Test operator = (const Test&);
~Test();
}

27.2 关于 string 的疑问

    string 类通过一个数据空间保存字符数据,string 类通过一个成员变量保存当前字符串长度,C++ 开发时尽量避开 C 语言中惯用的编程思想

28、智能指针

    一个特殊的指针,指针生命周期结束时主动释放堆空间,一片堆空间最多智能由一个指针标识,杜绝指针运算和指针比较,智能用来指向堆空间中的对象或者变量。

编程实现: 通过重载指针操作符(->和*),使用对象代替指针

28 逻辑操作符的陷阱

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
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
#include "stdafx.h"
#include <iostream>

using namespace std;

class Test
{
private:
int value;
public:
Test(int i)
{
value = i;
}

int getValue() const
{
return value;
}
};

bool operator && (const Test& l,const Test& r)
{
return l.getValue() && r.getValue();
}

bool operator || (const Test& l,const Test& r)
{
return l.getValue() || r.getValue();
}

Test func(Test t)
{
cout << "Test func(Test t): t.value = " << t.getValue() << endl;

return t;
}

int main()
{
Test t0(0);
Test t1(1);

if(func(t0) && func(t1)) // 等价于调用 operator &&( func(t0), func(t1)) 函数
{
cout << "Result is true!" << endl;
}
else
{
cout << "Result is false!" << endl;
}

cout << endl;

if(func(t0) || func(t1)) // 等价于调用 operator ||( func(t0), func(t1)) 函数
{
cout << "Result is true!" << endl;
}
else
{
cout << "Result is false!" << endl;
}

return 0;
}

运行结果:
Test func(Test t): t.value = 1
Test func(Test t): t.value = 0
Result is false!

Test func(Test t): t.value = 1
Test func(Test t): t.value = 0
Result is true!

不推荐重载逻辑与或 ,逻辑操作符重载后无法实现原生的语义
通过重载比较操作符代替逻辑操作符重载
直接使用成员函数代替逻辑操作符重载
使用全局函数对对逻辑操作符进行重载

29、逗号表达式的重载

    逗号表达式用于将多个字表达式连接为一个表达式,逗号表达式的值为最后一个子表达式的值,逗号表达式,的前N-1个子表达式可以没有返回值,逗号表达式按照从左向右的顺序计算每个子表达式的值

1
exp1 , exp2 , exp3 , ... , expN

在 C++ 中使用全局函数对逗号操作符进行重载,重载函数的参数必须有一个是类类型,重载函数的返回值必须是引用

1
2
3
4
Class& , (const Class& a, const Class& b)
{
return const_cast<Class&>(b);
}

建议使用全局函数重载逗号操作符

存在问题
C++通过函数调用扩展操作符的功能,进入函数体前必须完成所有参数的计算,函数参数的计算次序是不定的,重载后无法严格从左向右计算表达式,工程开发中不要重载逗号操作符

30、前置操作符与后置操作符的重载

    ++ 操作符可以被重载,全局函数和成员函数均可以进行重载,重载前置 ++ 操作符不需要额外的参数,重载后置 ++ 操作符需要一个 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
#include "stdafx.h"
#include <iostream>

using namespace std;

class Test
{
private:
int value;
public:
Test(int i)
{
value = i;
}

int getValue() const
{
return value;
}

Test& operator ++ () // 前置++实现
{
++value;
return *this;
}

Test operator ++ (int) //后置++实现
{
Test ret(value);

value++;

return ret;
}
};

int main()
{
Test t0(0);
Test t1 = t0++;

cout << t0.getValue() << endl;
cout << t1.getValue() << endl;

Test t2 = ++t0;

cout << t0.getValue() << endl;
cout << t2.getValue() << endl;

return 0;
}

运行结果:

31、类型转换函数

    构造函数可以定义不同类型的参数,参数满足下列条件时称为转换构造函数:有且仅有一个参数、参数是基本类型、参数是其他类类型

1
2
Test t;
t = 100;

    编译器会尽量尝试让源码通过编译,当将100赋值给对象t时,编译器首先看看有没有转换构造函数,如果有转换构造函数则进行转换

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
#include "stdafx.h"
#include <iostream>

using namespace std;

class Test
{
private:
int value;
public:
Test()
{
value = 0;
};

Test(int i) //转换构造函数
{
value = i;
}
};

int main()
{

int i = 0;

Test t ;

t = 5; //进行隐式类型转换,调用转换构造函数,==> t = Test(5)

return 0;
}

    编译器尽量尝试的结果是隐式类型转换:会让程序以意想不到的方式进行工作,是工程中 bug 的重要来源

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
#include "stdafx.h"
#include <iostream>

using namespace std;

class Test
{
private:
int value;
public:
Test(){};
Test(int i)
{
value = i;
}

int getValue() const
{
return value;
}

Test operator + (const Test& obj)
{
Test ret(value+ obj.value);

return ret;
}

};

int main()
{
int i = 0;

Test t ;

t = 5;

Test r;

r = t + 5; // ==> r = t + Test(5); 编译器自作聪明的编译通过,容易产生bug

cout << r.getValue() << endl;

return 0;
}

    C++ 编译器通过 explicit 关键字杜绝编译器的转换尝试,转换构造函数被 explicit 修饰时只能进行显示转换

1
2
3
static_cast<ClassName>(value)
ClassName(value)
(ClassName)value //不推荐

手动进行转换

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
// test.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include <iostream>

using namespace std;

class Test
{
private:
int value;
public:
Test()
{

};

explicit Test(int i)
{
value = i;
}

int getValue() const
{
return value;
}

Test operator + (const Test& obj)
{
Test ret(value+ obj.value);

return ret;
}

};

int main()
{
int i = 0;

Test t ;

t = static_cast<Test>(5); // 手动类型转换
Test r;

r = t + Test(5) + (Test)3; // 手动类型转换

cout << r.getValue() << endl;

return 0;
}

    C++ 中可以定义类型转换函数,类型转换函数用于将类对象转化为其他类型

1
2
3
4
5
6
operator Type ()
{
Type ret;

return ret;
}

    类型转换函数与转换构造函数具有同等的地位、使得编译器有能力将类对象转化为其他类型,编译器能够隐式使用类型转换函数

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
#include "stdafx.h"
#include <iostream>

using namespace std;

class Test
{
private:
int value;
public:
Test()
{

};

explicit Test(int i)
{
value = i;
}

int getValue() const
{
return value;
}

Test operator + (const Test& obj)
{
Test ret(value+ obj.value);

return ret;
}

operator int () //类型转换函数
{
return value;
}

};

int main()
{
int i = 0;
Test t(5);

i = t; //编译器隐式调用类型转换函数 == > t.operator int()

cout << i << endl;

return 0;
}

谨慎使用隐式类型转换,非常容易引入bug

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
#include "stdafx.h"
#include <iostream>

using namespace std;

class Tmp
{
public:
Tmp(int) //类型转换函数
{

}
};

class Test
{
private:
int value;
public:
Test()
{
value = 0;
};

operator Tmp () //转换构造函数
{
Tmp ret(0);

return ret;
}

};

int main()
{
Tmp i = 0;
Test t;

Tmp = t; // 转换构造函数与类型转换函数相互冲突

return 0;
}

    可以通过在转换构造函数前加explicit关键字解决,在实际工程中通常不使用转换构造函数而采用普通构造函数的方式实现转换

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
#include "stdafx.h"
#include <iostream>

using namespace std;

class Tmp
{
public:
Tmp(int)
{

}
};

class Test
{
private:
int value;
public:
Test()
{
value = 0;
};

Tmp toTmp()
{
Tmp ret(0);

return ret;
}

};

int main()
{
Tmp i = 0;
Test t;

i = t.toTmp(); // 通过构造函数实现类的类型转换

return 0;
}

32、继承的概念和意义

    面向对象中的继承指类之间的父子关系:子类拥有父类的所有属性和行为、子类就是一种特殊的父类、子类对象可以当做父类对象使用、子类中可以添加父类没有的方法和属性

1
2
3
4
5
6
7
8
9
class Parent
{

};

class Child : public Parent //描述继承关系
{

};

    子类是一个特殊的父类,子类对象可以直接初始化父类对象,子类对象可以直接赋值给父类对象。继承是 C++ 中代码复用的重要手段。通过继承可以获得父类所有功能,并且可以在子类中重写已有功能,或者添加新功能。

33、继承中的访问级别

    面向对象中的访问级别不知是 public 和 private,可以定义 protected 访问级别:protected修饰的成员不能被外界直接访问、修饰的成员可以被子类直接访问

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
#include "stdafx.h"
#include <iostream>

using namespace std;

class Parent
{
protected : //protected 访问级别
int mv;
public:
Parent()
{
mv = 10;
}

int value()
{
return mv;
}

};

class Child : public Parent //描述继承关系
{
public:
int addValue(int mv)
{
return Parent::mv = Parent::mv + mv; //子类直接访问父类私有成员
}
};

int main()
{
Child c;

cout << c.value() << endl;

cout << c.addValue(8) << endl;

return 0;
}

34、定义类时访问级别的选择

插图44课

35、不同的继承方式

    public 继承:父类成员在子类中保持原有的访问级别。private 继承:父类成员在子类中变为私有成员。protected 继承:父类中的公有成员变为保护成员,其他成员保持不变

插图44课

继承成员的访问属性 = Max{继承方式,父类成员访问属性}

    C++ 中的默认继承方式为 private,一般而言,C++工程项目中只使用 public 继承,C++ 的派生语言只支持一种继承方式(public 继承),protected 和 private 继承带来的复杂性远大于使用性

36、继承中的构造与析构

    子类可以定义构造函数,子类的构造函数必须对继承而来的成员进行初始化:直接通过初始化列表或者赋值的方式进行初始化、调用父类的构造函数进行初始化

    父类构造函数在子类中的调用方式:默认调用,适用于无参构造函数和使用默认参数的构造函数、显示调用,通过初始化列表进行调用,适用于所有父类构造函数

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
#include "stdafx.h"
#include <iostream>
#include <string>
using namespace std;

class Parent
{
public:
Parent()
{
cout << "Parent()" << endl;
}

Parent(string s)
{
cout << "Parent(string s) : " << s << endl;
}
};

class Child : public Parent //描述继承关系
{
public:
Child() //隐式调用父类构造函数Parent()
{
cout << "Child()" << endl;
}

Child(string s)
: Parent("Parameter to Parent") //显示调用父类构造函数Parent(string s)
{
cout << "Child() : " << s << endl;
}
};

int main()
{
Child c;

cout << endl;

Child cc("hello");

return 0;
}

运行结果:
Parent()
Child()

Parent(string s) : Parameter to Parent
Child() : hello

构造规则:子类对象在创建时会首先调用父类的构造函数、先执行父类构造函数
再执行子类的构造函数、父类构造函数可以被隐式调用或者显示调用

对象创建时构造函数的调用顺序

  1. 调用父类的构造函数
  2. 调用成员变量的构造函数
  3. 调用类自身的构造函数

口诀心法:
先父母、后客人、再自己

析构函数的调用顺序与构造函数相反

  1. 执行自身的析构函数
  2. 执行成员变量的析构函数
  3. 执行父类的析构函数

37、父子间的冲突

子类可以定义父类中的同名成员,子类中的成员将隐藏父类中的同名成员,父类中的同名成员依然存在于子类中,子类可以定义父类中的同名函数,使用作用域分辨符::访问父类中的同名成员和同名函数

特殊的同名函数
子类中可以定义父类中已经存在的成员函数,这种定义发生在继承中,叫函数重写,函数重写是同名覆盖的一种特殊情况

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
#include "stdafx.h"
#include <iostream>
#include <string>
using namespace std;

class Parent
{
public:
int mi;

int Get()
{
return mi;
}
};

class Child : public Parent //描述继承关系
{
public:
int mi;

int Get()
{
return mi;
}
};

int main()
{
Child c;

c.mi = 100;
c.Parent::mi = 10; //调用父类中的成员变量

cout << c.Get() << endl;
cout << c.Parent::Get() << endl; //调用父类同名的函数

return 0;
}

运行结果:
100
10

父子间的赋值兼容
子类对象可以当做父类对象使用(兼容性):子类对象可以直接赋值给父类对象使用、子类对象可以直接初始化父类对象、父类指针可以直接指向子类对象、父类引用可以直接引用子类对象

当使用父类指针(引用)指向子类对象时,子类对象退化为父类对象,只能访问父类中定义的成员,可以访问被子类覆盖的同名成员

38、多态的概念和意义

    根据实际的对象类型决定函数调用的具体目标,同样的语句在实际运行时有多种不同的表现形式

    C++ 语言直接支持多态的概念,通过使用 virtual 关键字对多态进行支持,被 virtual 声明的函数被重写后具有多态特性,被 virtual 声明的函数叫做虚函数

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
#include "stdafx.h"
#include <iostream>
#include <string>
using namespace std;


class Grandfather
{
public:
void print() //通过关键字virtual实现多态
{
cout << "I'm Grandfather" << endl;
}
};

class Parent : Grandfather
{
public:
virtual void print() //通过关键字virtual实现多态
{
cout << "I'm Parent" << endl;
}
};

class Child_A : public Parent //描述继承关系
{
public:
void print()
{
cout << "I'm Child A" << endl;
}
};

class Child_B : public Parent //描述继承关系
{
public:
void print()
{
cout << "I'm Child B" << endl;
}
};

int main()
{
Child_A a;
Child_B b;

Parent* pa = &a;
pa->print();

Parent& A = a;
A.print();

cout << endl;

Parent* pb = &b;
pb->print();

Parent& B = b;
B.print();

return 0;
}

运行结果:
I'm Child A
I'm Child A

I'm Child B
I'm Child B

多态的意义
在程序运行中展现出动态特性、函数重写必须多态实现,否则没有意义、多态是面向对象组件化和程序设计的基础特性

理论中的概念
静态联编,在程序编译期间就能确定具体的函数调用,如函数重载
动态联编,在程序实际运行后才能确定具体的函数调用,如函数重写

39、对象模型

class是一种特殊的struct,在内存中class依旧可以看做变量的集合,class与struct遵循相同的内存对齐规则,class中的成员函数与成员变量是分开存放的:每个对象有独立的成员变量、所有类对象共享类中的成员函数

运行时的对象退化为结构体的形式,所有的成员变量在内存中依次排布,成员变量间可能存在内存空隙,可以通过内存地址直接访问成员变量,访问权限关键字在运行时失效

继承对象模型
在C++编译器的内部类可以理解为结构体,子类是由父类成员叠加子类新成员得到的

C++多态的实现原理
当类中声明虚函数时,编译器会在类中生成一个虚函数表,虚函数表是一个存储成员函数地址的数据结构,虚函数表是由编译器自动生成与维护的,virtual成员函数会被编译器放入虚函数表中,存在虚函数时,每个对象中都有一个指向虚函数表的指针