├── .gitignore ├── C++ ├── 1-概述.md ├── 10-字符串创建与使用.md ├── 11-cstring函数大全.md ├── 12-string用法.md ├── 13-结构体初探.md ├── 14-枚举类型.md ├── 15-指针初探.md ├── 16-指针初探(二).md ├── 17-指针初探(三).md ├── 18-内存模型初探.md ├── 19-for循环.md ├── 2-常用语句.md ├── 20-自增与自减.md ├── 21-while与do while循环.md ├── 22-二维与多维数组.md ├── 23-if语句.md ├── 24-逻辑表达式.md ├── 25-三元表达式.md ├── 26-switch语句.md ├── 27-break和continue语句.md ├── 28-指针和const.md ├── 29-函数指针.md ├── 3-谷歌编码规范.md ├── 30-函数指针进阶.md ├── 31-内联函数.md ├── 32-引用.md ├── 33-引用与const.md ├── 34-引用和指针的区别.md ├── 35-引用结构体.md ├── 36-默认参数.md ├── 37-函数重载.md ├── 38-右值引用.md ├── 39-函数模板.md ├── 4-整型.md ├── 40-重载模板.md ├── 41-模板显式具体化.md ├── 42-模板实例化.md ├── 43-编写头文件.md ├── 44-联合编译.md ├── 45-自动存储连续性.md ├── 46-auto关键字.md ├── 47-全局变量.md ├── 48-内部链接性.md ├── 49-函数和语言链接性.md ├── 5-long long与__int64.md ├── 50-存储方案和动态分配.md ├── 51-名称空间.md ├── 52-using声明.md ├── 53-using声明和using指令.md ├── 54-名称空间其他特性.md ├── 55-初探面向对象.md ├── 56-类的定义.md ├── 57-类的实现.md ├── 58-构造函数.md ├── 59-默认构造函数.md ├── 6-char类型与io加速.md ├── 60-析构函数.md ├── 61-this指针.md ├── 62-类枚举.md ├── 63-抽象数据类型.md ├── 64-运算符重载.md ├── 65-重载限制.md ├── 66-友元函数.md ├── 67-重载<<运算符.md ├── 68-类的转换.md ├── 69-转换函数.md ├── 7-浮点数.md ├── 70-构造函数的一些坑.md ├── 71-拷贝构造函数.md ├── 72-赋值运算符.md ├── 73-成员初始化列表.md ├── 74-继承(一).md ├── 75-继承(二).md ├── 76-继承(三).md ├── 77-多态.md ├── 78-静态联编和动态联编.md ├── 79-虚函数.md ├── 8-算术运算符与类型转换.md ├── 80-protected.md └── 9-数组的定义和初始化.md ├── LeetCode ├── 01-two sum.md ├── 02-add two number.md ├── 04-median of two sorted arrays.md ├── 05-longest palindromic substring.md ├── 10-regular expression matching.md ├── 11-container with most water.md └── 15-3sum.md ├── README.md └── resource ├── EasyLeetCode.png └── wechat_qrcode.jpg /.gitignore: -------------------------------------------------------------------------------- 1 | test -------------------------------------------------------------------------------- /C++/1-概述.md: -------------------------------------------------------------------------------- 1 | 我们先来看一段 C++的示例代码: 2 | 3 | ```C++ 4 | // my first cpp file 5 | #include 6 | int main() { 7 | using namespace std; 8 | cout << "hellworld" << endl; 9 | return 0; 10 | } 11 | ``` 12 | 13 | 这段代码虽然很短,但几个重要的要素都包含了,如果只是用来刷题,会发现基本上也只会用到这些语法。 14 | 15 | 我们一行一行来看。 16 | 17 | ### 注释 18 | 19 | 首先是第一行,第一行表示的 C++当中的注释。C 语言当中的`/**/`的多行注释方法同样支持,但为了避免潜在的问题,尽量使用 C++类型的注释。 20 | 21 | ### include 22 | 23 | 第二行是预编译指令 include,这里的预编译指令是一个专业名词,表示在编译之前预先执行的指令。C++当中的预编译指令有好几种,除了 include 之外还有 define、ifdef、undef 等等,我们后面遇到了再说。 24 | 25 | include 这个指令会引入一个源代码文件,后面跟着的是一个包含文件名,也叫头文件名。C 语言中的头文件以.h 结尾,而 C++当中兼容了 C 语言的头文件,只不过将其重新命名,去掉了末尾的.h,而换成了 c 开头。比如 math.h 文件替换成了 cmath。 26 | 27 | 在这行代码当中我们 include 的头文件叫做 iostream,iostream 为 C++的标准输入输出库,用来在终端读入或者打印文本信息。iostream 可以拆开理解成 io 和 stream,其中 io 表示 input、output 即输入输出,stream 表示流,即使用流形式进行 io。 28 | 29 | ### main 函数 30 | 31 | main 函数是 C++程序的入口函数,C++标准当中定义的 main 函数类型是 int。返回 0 表示程序正常退出,所以一般我们要在 main 函数的最后写上 return 0。 32 | 33 | C++的 main 函数通常有两种写法,一种是参数留空,一种是定义参数数量以及参数值。 34 | 35 | ```C++ 36 | int main() {} 37 | int main(int argc, char* argv[]) {} 38 | ``` 39 | 40 | 关于 main 函数的传参方法,以及参数使用方法同样会在之后介绍,初学者随便使用哪一种都是一样的。 41 | 42 | 注意,有些编译器比如 vc6.0 等没有严格遵循 gcc 规范,导致支持 void 类型的 main 函数。建议放弃此类不规范的编译器,以免代码无法迁移,并且养成不好的编码习惯。 43 | 44 | ### 命名空间 45 | 46 | 命名空间是 C++当中的特性,用来解决大型项目名称冲突的问题。 47 | 48 | 有可能多个程序员同时开发了 getValue 函数,但当我们编译运行的时候,编译器无法知道我们究竟调用的是哪一个 getValue,所以需要使用命名空间进行区分。比如 A 程序员将 getValue 放入了名为 A 的命名空间当中,那么当我们调用 getValue 的时候就需要写成 A::getValue,B 程序员将它放入了名为 B 的命名空间中,同样调用的时候写成 B::getValue。 49 | 50 | 然而常用的许多函数、变量都明明在 std 的命名空间当中,这就意味着当我们使用这类内容的时候,都需要在前面加上 std::,比如标准输出的 cout 命令,需要写成 std::cout。 51 | 52 | 这会导致编码变得繁琐,所以 C++提供了 using namespace 的功能,即告诉编译器当前使用的命名空间名称,这样我们在调用该命名空间的内容时,可以省略前缀。 53 | 54 | 除了 using namespace 之外我们也可以单独使用 using 命令,例如: 55 | 56 | ```C++ 57 | using std::cout; 58 | using std::cin; 59 | ``` 60 | 61 | ### cout 62 | 63 | 标准输出工具,可以将字符串输出在终端中。 64 | 65 | cout 本身是一个预定义的对象,它知道如何显示字符串、数字和单个字符。<<符号表示将字符串发送给 cout,由 cout 输出在屏幕当中。<<符号表示了信息流动的路径,<<符号可以多个叠加连接,比如: 66 | 67 | ```C++ 68 | cout << "hello" << "world"; 69 | ``` 70 | 71 | endl 表示当前输出行的结束,cout 遇到 endl 时会重启一行,否则则会接在同一行后面继续输出。 72 | 73 | 除了使用 endl 之外,我们也可以使用换行符来达到同样的效果,如: 74 | 75 | ```C++ 76 | cout << "hello" << "world\n"; 77 | ``` 78 | 79 | C++ Primer 当中建议在输出由内容的字符串时使用换行符,而非 endl,其他情况下使用 endl,而非换行符。这两者绝大多数情况下是等价的,在一些特殊情况下可能会构成差异。 80 | 81 | 如 endl 确保程序继续运行前刷新输出,而使用"\n"则意味着有时候需要在输入信息之后才会出现提示。当然这不是重点,我们遇到的概率也非常小,如果实在搞不清楚也不用纠结,统一使用 endl 也行。 82 | -------------------------------------------------------------------------------- /C++/10-字符串创建与使用.md: -------------------------------------------------------------------------------- 1 | ## 字符串 2 | 3 | ### 字符串定义 4 | 5 | 字符串就是连续的一连串字符,在 C++当中, 处理字符串的方式有两种类型。一种来自于 C 语言,也被称为 C 风格字符串。另外一种是基于`string`类库。 6 | 7 | C 风格的字符串其实就是字符存储在 char`数组`当中。不过它和一般的数组有一些区别,拥有一些特殊的性质。比如一空字符`\0`结尾,它的 ascii 码是 0,用来标记字符串的结尾。 8 | 9 | ```C++ 10 | char str[5] = {'h', 'e', 'l', 'l', 'o'}; 11 | char str2[5] = {'h', 'e', 'l', 'l', '\0'}; 12 | ``` 13 | 14 | 对于上面的两个例子,第一个例子虽然也是 char 数组,但是由于它的结尾不是`\0`,所以它不能看成是字符串。因为很多算法都是以`\0`的位置为标记的,比如计算字符串长度的算法,以及 cout 等等。 15 | 16 | 上面我们采用的是数组常规的初始化方式,这当然是可以的,不过这样会很不方便。一个是需要一位一位地填写字符,会非常地麻烦。另外还需要手动填充`\0`,也容易忘记,所以对于字符串而言我们还有更好的初始化方式: 17 | 18 | ```C++ 19 | char hello[6] = "hello"; 20 | char world[] = "world"; 21 | ``` 22 | 23 | 用引号括起来的字符串隐式地包含了结尾的`\0`,需要注意的是,我们在确定数组长度的时候需要将结尾的`\0`也计算在内。 24 | 25 | 这里要提醒大家注意引号的区别, 在 C++当中单引号表示单个字符,而双引号表示字符串。所以下面这种写法是错误的: 26 | 27 | ```C++ 28 | char c = "S"; 29 | ``` 30 | 31 | 并且“S”其实表示的是字符串所在的内存地址,当我们把一个内存地址赋值给一个`char`类型的时候自然就会报错了。 32 | 33 | 咦,不是说好的是字符串么,怎么又扯到地址了?不要急,等后面讲到指针的地方就明白了。 34 | 35 | ### 字符串的读入 36 | 37 | 直接用字符串常量来初始化字符数组只是一种方式,另外一种常用的方式是只定义字符数组的长度,从外部读入数据,如: 38 | 39 | ```C++ 40 | char str[100]; 41 | 42 | scanf("%s", str); 43 | cin >> str; 44 | ``` 45 | 46 | 无论是使用`scanf`还是`cin`,都是一样的效果。 47 | 48 | 但是没有这么简单,比如我们再来看一段代码: 49 | 50 | ```C++ 51 | char name[100]; 52 | char level[100]; 53 | 54 | scanf("%s", name); 55 | scanf("%s", level); 56 | ``` 57 | 58 | 在这段代码当中,我们定义了 name 和 level 两个字符串变量。当我们执行的时候,就会发现问题: 59 | 60 | ![](https://tva1.sinaimg.cn/large/008i3skNgy1gvmpp92pz8j611q034wep02.jpg) 61 | 62 | 我刚输入完名字,还没来得及输 level 就结束了。如果我们把 name 和 level 分别输出的话就会发现,name 的值是 liang,level 的值是 tang。 63 | 64 | 这说明了什么?说明了我们读入字符串的时候它并不是按行读入的,而是按照空格分隔的!它不像是隔壁的 Python,input 默认就是读入一行,C++的读入默认都是按照空白字符分隔的,除了空格之外,也包括`\t`,空字符等等。 65 | 66 | 那问题来了,假如我们需要读入一行应该怎么办呢?也有办法,我们可以使用`cin.getline`代替之前的`scanf`或者是`cin`。我们来看下它的函数签名: 67 | 68 | ```C++ 69 | istream& getline ( istream& is, string& str, char delim ); 70 | istream& getline ( istream& is, string& str ); 71 | ``` 72 | 73 | C++允许参数列表不同的同名函数重载,这两个签名都是 OK 的。两者的差别在于第三个参数,但三个参数表示分隔符,如果不传的话,默认是`'\n'`。第二个参数表示字符串的长度,所以如果要按照行来读入字符串的话,刚刚的代码应该写成: 74 | 75 | ```C++ 76 | cin.getline(name, 100); 77 | cin.getline(level, 100); 78 | ``` 79 | 80 | 除了可以使用 getline 之外,还可以使用 get。get 有好几种变体,一种变体是读入一个字符,它有一种变体也可以读入一行字符串。不过唯一的区别是,get 函数不会处理行尾的换行符。如果我们要读入两行字符的话,需要手动将这个换行符处理掉。 81 | 82 | ```C++ 83 | cin.get(name, 100); // 读入一行数据 84 | cin.get(); // 读入换行符 85 | cin.get(level, 100);// 读入第二行数据 86 | ``` 87 | 88 | 写成三行看起来有些繁琐,我们还可以进行简化,简化成一行: 89 | 90 | ```C++ 91 | cin.get(name, 100).get().get(level, 100); 92 | ``` 93 | 94 | 看起来很像是 Java8 的流式编程,能够这样做的原因是 get 和 getline 函数会返回一个 cin 的对象。所以我们可以这样连续调用。 95 | 96 | 相信有些同学已经注意到了,同样的函数名,根据我们传入的参数不同执行了不同的逻辑。这在 C++当中叫做函数重载,是一个非常重要的概念。 97 | 98 | ### 排坑 99 | 100 | 关于 getline 有一个比较大的坑,当我们同时使用 cin 和 getline 的时候,有时候会出现问题。比如下面这段代码: 101 | 102 | ```C++ 103 | int a; 104 | char name[100]; 105 | cin >> a; 106 | cin.getline(name, 100); 107 | 108 | cout << "a = " << a << endl; 109 | cout << "name = " << name << endl; 110 | ``` 111 | 112 | 这段代码很简单,我们定义了两个变量。一个是`int`型的 a,一个是字符串 name。我们使用 cin 读入 a,使用 getline 读入 name。 113 | 114 | 这看起来一点问题也没有,但是当我们运行的时候就会出现问题。 115 | 116 | ![](https://tva1.sinaimg.cn/large/008i3skNgy1gvnu7prx5vj610w054t8x02.jpg) 117 | 118 | 会发现我都没有来得及输入 name,程序就结束了,而 name 读到了一个空。 119 | 120 | 这并不是 C++有 bug,而是我们在输入 32 的时候,敲了一个回车。所以在使用 getline 读入一行的时候,看到了回车,直接退出了,读入了一个空行,这就是为什么我们没有机会输入 name 的原因。 121 | 122 | 要解决这个问题怎么办呢?其实也很简单,我们额外读入一个字符,把换行符给读取掉就行了。 123 | 124 | ```C++ 125 | int a; 126 | char name[100]; 127 | cin >> a; 128 | cin.get(); // getchar() C语言版本 129 | cin.getline(name, 100); 130 | 131 | cout << "a = " << a << endl; 132 | cout << "name = " << name << endl; 133 | ``` 134 | 135 | 类似的问题在竞赛的题目当中很常见,我们经常要同时读入字符串和数字,很容易遇到这样的问题。遇到了不要紧张,仔细检查一下数据和逻辑,看看是不是读入到了换行符。 136 | -------------------------------------------------------------------------------- /C++/11-cstring函数大全.md: -------------------------------------------------------------------------------- 1 | ## cstring.h 常用函数 2 | 3 | cstring.h 库即 C 语言中的 string.h 库,它是 C 语言中为字符串提供的标准库。C++对此进行了兼容,所以我们在 C++当中一样可以使用。 4 | 5 | 这个库当中有大量的关于字符串操作的 api,本文选择了其中最常用的几个进行阐述。 6 | 7 | ### strlen 8 | 9 | 由于编译器是按照`\0`的位置来确定字符串的结尾的,所以字符串的长度并不等于数组的长度。我们可以使用`strlen`函数求得字符串的真实长度: 10 | 11 | ```C++ 12 | char name[100] = "hello world"; 13 | cout << strlen(name) << endl; 14 | ``` 15 | 16 | 比如我们这里用一个长度为 100 的`char`数组存储了“helloworld”字符串,当我们使用`strlen`函数求它的实际长度只有 11。 17 | 18 | ### strcat 19 | 20 | `strcat`函数可以将两个字符串进行拼接,它的函数签名为: 21 | 22 | ```C++ 23 | char *strcat(char *dest, const char *src) 24 | ``` 25 | 26 | 我们可以看到它接受两个参数,一个是`dest`,一个是`src`,都是`char*`类型,返回的结果也为`char *`类型。在 C++当中,数组名是指向数组中第一个元素的常量指针。所以虽然签名中写的参数是指针类型,但我们传入数组名同样可以。 27 | 28 | 我们传入两个字符串之后,`strcat`函数会将`src`字符串拼接在`dest`字符串末尾,并且返回指向拼接之后结果的指针。所以下面两种方式输出结果得到的值是一样的。 29 | 30 | ```C++ 31 | char name[100] = "hello world"; 32 | char level[100] = "concat test"; 33 | 34 | char *ret = strcat(name, level); 35 | cout << ret << endl; // 方式1 36 | cout << name << endl; // 方式2 37 | ``` 38 | 39 | ### strncat 40 | 41 | `strcat`函数的变种,函数额外多接收一个参数控制拷贝`src`字符串的最大长度。 42 | 43 | ```C++ 44 | char *strncat(char *dest, const char *src, size_t n) 45 | ``` 46 | 47 | 我们使用刚才同样的例子: 48 | 49 | ```C++ 50 | char name[100] = "hello world"; 51 | char level[100] = "concat test"; 52 | 53 | char *ret = strncat(name, level, 4); 54 | cout << ret << endl; 55 | cout << name << endl; 56 | ``` 57 | 58 | 由于我们传入了 4,限制了 level 字符串拷贝的长度,所以最终得到的结果为:`hello worldconc`。 59 | 60 | ### strcpy 61 | 62 | 字符串拷贝函数,可以将`src`字符串中的内容复制到`dest`。 63 | 64 | ```C++ 65 | char *strcpy(char *dest, const char *src) 66 | ``` 67 | 68 | 使用方法和前面介绍的其他函数类似,有两点需要注意。 69 | 70 | 1. 如果`dest`字符串长度不够长,在编译时不会报错,但运行时可能导致问题。 71 | 72 | ```C++ 73 | char name[10] = ""; 74 | char level[100] = "concat test"; 75 | 76 | strcpy(name, level); 77 | cout << name << endl; 78 | ``` 79 | 80 | 上面这段代码可以编译运行,但是运行的时候终端会出现出错信息。 81 | 82 | ![](https://tva1.sinaimg.cn/large/008i3skNgy1gvoxy0gkkkj610q042gm002.jpg) 83 | 84 | 所以在使用`strcpy`的时候千万小心,一定要保证`dest`有足够长度。 85 | 86 | 2. 如果`dest`中原本就有内容,会被覆盖。 87 | 88 | ```C++ 89 | char name[15] = "abc"; 90 | char level[100] = "concat test"; 91 | 92 | strcpy(name, level); 93 | cout << name << endl; 94 | ``` 95 | 96 | 运行完`strcpy`之后,`name`中的内容会被清空。 97 | 98 | ### strncpy 99 | 100 | `strcpy`加入长度限制的版本,可额外多传入一个参数 n 表示最多赋值 n 个字符。当`src`长度小于 n 时,剩余部分将会使用空字节填充。 101 | 102 | ```C++ 103 | char *strncpy(char *dest, const char *src, size_t n) 104 | ``` 105 | 106 | ```C++ 107 | char name[15] = "abc"; 108 | char level[100] = "concat test"; 109 | 110 | strncpy(name, level, 4); 111 | cout << name << endl; 112 | ``` 113 | 114 | 输出结果为`conc`。 115 | 116 | ### memset 117 | 118 | 字符串的批量设置函数,可以将字符串批量设置成某一个字符。 119 | 120 | ```C++ 121 | void *memset(void *str, int c, size_t n) 122 | ``` 123 | 124 | `int c`表示要被设置的字符,`size_t n`表示设置的字符数量。 125 | 126 | ```C++ 127 | char name[15] = "abc"; 128 | char level[100] = "concat test"; 129 | 130 | memset(name, 'c', 10); 131 | cout << name << endl; 132 | ``` 133 | 134 | 上述代码的运行结果为 10 个 c。 135 | 136 | 多说一句,memset 除了可以用来给字符串进行批量设置之外也可以给 int 型的数组进行批量设置。由于一个 32 位的 int 占据 4 个字节,也就是 4 个字符长度。所以使用`memset`进行批量设置的时候,最终得到的结果将是 4 个传入的`int c`拼接的结果。 137 | 138 | ```C++ 139 | int a[100]; 140 | memset(a, 1, sizeof a); // memset(a, 1, 400); 因为一个int占据4个字节 141 | ``` 142 | 143 | 我们这样设置之后,a 数组当中的元素并不是 1,而是`0x01010101`,转成 10 进制是 16843009。所以使用`memset`对 int 型数组进行初始化一般只用 3 种操作: 144 | 145 | ```C++ 146 | // 1. 批量设置成0 147 | memset(a, 0, sizeof a); 148 | // 2. 批量设置成-1 149 | memset(a, -1, sizeof a); 150 | // 3. 批量设置成一个接近最大整数上限的值 151 | memset(a, 0x7f, sizeof a); 152 | memset(a, 0x3f, sizeof a); 153 | ``` 154 | 155 | 关于`memset`使用的一些具体细节将在后续题解的实际问题当中再做详细说明。 156 | 157 | ### memcpy 158 | 159 | 和`strcpy`类似,从`str2`中复制 N 个字符到`str1`中。 160 | 161 | ```C++ 162 | void *memcpy(void *str1, const void *str2, size_t n) 163 | ``` 164 | 165 | ```C++ 166 | char name[15] = "abc"; 167 | char level[100] = "concat test"; 168 | 169 | memcpy(name, level, 10); 170 | ``` 171 | 172 | ### strcmp 173 | 174 | 将两个字符串按照字典顺序进行比较,对于字典序的顺序定义为:两个字符串自左向右逐个字符相比(按 ASCII 值大小相比较),直到出现不同的字符或遇 **\0** 为止。 175 | 176 | ```C++ 177 | int strcmp(const char *str1, const char *str2) 178 | ``` 179 | 180 | 返回的结果为一个 int,如果它小于 0,说明`str1`小于`str2`,如果它等于 0,说明两者相等,如果大于 0,说明`str1`大于`str2`。 181 | 182 | ```C++ 183 | char name[15] = "abc"; 184 | char level[100] = "abcd"; 185 | 186 | cout << strcmp(name, level) << endl; 187 | ``` 188 | 189 | 运行结果为-100,说明 name 小于 level。 190 | 191 | ### strncmp 192 | 193 | `strcmp`长度限制版,可以额外接受一个数字 n,表示最多比较长度为 n 的字符。 194 | 195 | ```C++ 196 | int strncmp(const char *str1, const char *str2, size_t n) 197 | ``` 198 | 199 | ### strstr 200 | 201 | ```C++ 202 | char *strstr(const char *haystack, const char *needle) 203 | ``` 204 | 205 | 在`haystack`中搜索`needle`第一次出现的位置,返回指向该位置的指针。 206 | 207 | ```C++ 208 | char name[15] = "search-test"; 209 | char level[100] = "-"; 210 | 211 | char *ret = strstr(name, level); 212 | cout << ret << endl; 213 | ``` 214 | 215 | 运行之后,屏幕输出的结果为: 216 | 217 | ![](https://tva1.sinaimg.cn/large/008i3skNgy1gvoyx1hwssj60yo036weo02.jpg) 218 | 219 | 因为当我们使用 cout 输出一个`char *`变量的时候,它会当做是字符串进行输出,即一直输出字符,直到遇见`\0`为止。 220 | 221 | 输出的结果为-test,说明我们通过`strstr`函数找到了“-”出现的位置,跳过了之前的内容。 222 | 223 | 除了上文介绍的这些函数之外,`cstring`当中还有很多其他的 api,例如`strtok`,`memmove`等等,大家感兴趣不妨去翻阅相关文档,也许会有惊喜。 224 | -------------------------------------------------------------------------------- /C++/12-string用法.md: -------------------------------------------------------------------------------- 1 | ## string 类 2 | 3 | ### 和 char \*的异同 4 | 5 | 在 C++当中,除了`char *`类型,还有专门的字符串类型,就叫做`string`。通过包含头文件`string`就可以使用: 6 | 7 | ```C++ 8 | include 9 | ``` 10 | 11 | 在很多方面,`string`类型的使用方法和`char *`一样,例如: 12 | 13 | ```C++ 14 | string str1; 15 | string str2 = "hello world"; // 初始化和char *一致 16 | 17 | cout << str2 << endl; // cout和char *一致 18 | cout << str[2] << endl; // 元素访问和char *一致 19 | ``` 20 | 21 | 除此之外,它还有一个更大的好处,就是当我们从外界读入字符串的时候,再也不用操心读入的字符串超界的问题了。因为`string`类设计可以让程序自动处理字符串的大小。 22 | 23 | ```C++ 24 | string str1; 25 | cin >> str1; 26 | ``` 27 | 28 | ### C++11 初始化 29 | 30 | 我们也可以使用 C++11 的列表初始化特性在`string`上,不过其实没必要,直接使用等于号赋值更方便一些: 31 | 32 | ```C++ 33 | string str1 = {"hello world"}; 34 | string str2 {"test"}; 35 | ``` 36 | 37 | ### 拼接 38 | 39 | 在使用`char *`的时候,如果我们要拼接两个字符串,需要调用函数`strcat`来实现。而有了`string`,我们可以直接使用加号来拼接两个字符串: 40 | 41 | ```C++ 42 | string str1 = "hello"; 43 | string str2 = "world"; 44 | 45 | string str3 = str1 + str2; 46 | ``` 47 | 48 | 不仅如此,还可以使用+=,表示把另外一个字符串添加在自己末尾: 49 | 50 | ```C++ 51 | str1 += str2; // 等价于str1 = str1 + str2 52 | ``` 53 | 54 | ### 长度 55 | 56 | 对于`char *`的字符串,我们要求长度,需要使用`strlen`函数。而`string`类型的字符串,我们可以直接调用.size()函数: 57 | 58 | ```C++ 59 | string str = "hello"; 60 | 61 | cout << str.size() << endl; // output 5 62 | ``` 63 | 64 | ### IO 65 | 66 | 前文说了,`string`类的 cin 和 cout 用法都和`char *`完全一致,不过在读取一行的时候有些区别。 67 | 68 | ```C++ 69 | char st[100]; 70 | cin.getline(st, 100); 71 | 72 | string str; 73 | getline(cin, str); 74 | ``` 75 | 76 | 对于`char *`来说,我们调用的`cin.getline`表示的是 cin 中的一个方法。而后者,我们调用的`getline`传入了 cin,这里的 cin 是一个传入的参数,并且也没有指定长度,因为`string`会自动设定长度。 77 | 78 | ### 原始字符串 79 | 80 | 关于`string`类型,C++11 有一个原始字符串的新特性。 81 | 82 | 在字符串表示当中,当我们要添加一些特殊字符的时候,往往需要在前面加上反斜杠,表示取义。当这类字符多了之后,就会很麻烦: 83 | 84 | ```C++ 85 | cout << "i want to output \"hello world\"" << endl; 86 | ``` 87 | 88 | 如果我们要换行,还要在其中加入`\n`。针对这个问题 C++11 提出了原始字符串,也就是说在原始字符串当中所有的符号都会被原本原样地输出,不需要再使用`\`来取义了。 89 | 90 | 原始字符串以`"R(`开头`)"`结尾,`比如刚才的内容就可以写成: 91 | 92 | ```C++ 93 | cout << R"(i want to output "hello world")" << endl; 94 | ``` 95 | 96 | 不仅如此,我们还可以随意换行: 97 | 98 | ```C++ 99 | cout << R"(i want to output 100 | "hello world")" << endl; 101 | ``` 102 | 103 | 但是有一个小问题,假如我们想要输出的结果当中也包含`"(`该怎么办呢?也有办法,编译器允许我们在`"`和`(`之间加入任意的字符来做区分(空格、左括号、右括号、斜杠、控制字符除外),这样我们在结尾的时候,也需要加上同样的字符。例如: 104 | 105 | ```C++ 106 | cout << R"tst("(test)")tst" << endl; 107 | ``` 108 | 109 | 运行之后,屏幕上会输出"(test)"的结果。 110 | -------------------------------------------------------------------------------- /C++/13-结构体初探.md: -------------------------------------------------------------------------------- 1 | ## 结构体 2 | 3 | ### 定义结构体 4 | 5 | 数组可以存储多个同类型的变量,但如果我们想要存储多个不同类型的变量呢?比如说我们想要存储一个学生的姓名、年龄、性别、考试分数,这些变量可能`string`, `int`, `float`都有,显然就不能使用数组了。 6 | 7 | 为了满足这样的存储要求,我们可以使用 C++当中的结构体(struct)。在同一个结构体当中,我们可以定义许多种不同类型的变量,就可以满足我们各种各样的需求了。如果我们想要存储多个这样的内容,还可以将它定义成数组。 8 | 9 | 结构体和类已经很接近了,所以理解结构体对于理解面向对象非常有帮助。虽然算法竞赛当中一般用不到面向对象,但是对于开发者来说,面向对象可以说是必学的内容。C++ primer 中说,结构体是 C++ OOP(面向对象编程)的基石。 10 | 11 | 我们使用关键字`struct`来定义一个结构体: 12 | 13 | ```C++ 14 | struct student { 15 | string name; 16 | bool gender; 17 | double scores; 18 | }; 19 | ``` 20 | 21 | 表示定义了一个结构体,它的类型名是 student,花括号当中括起来的内容是结构体的成员变量。注意 student 是类型名,也就是说我们可以用它来定义出 student 类型的变量: 22 | 23 | ```C++ 24 | student xiaoming; 25 | student john; 26 | ``` 27 | 28 | 我们可以用`.`来访问结构体内部的元素: 29 | 30 | ```C++ 31 | cout << john.name << endl; 32 | ``` 33 | 34 | `struct`的定义有两种写法,既可以写在 main 函数外部,也可以写在 main 函数内部。如: 35 | 36 | ```C++ 37 | // 写法1 38 | struct student { 39 | string name; 40 | bool gender; 41 | double scores; 42 | }; 43 | 44 | int main() { 45 | // do something 46 | return 0; 47 | } 48 | 49 | // 写法2 50 | int main() { 51 | struct student { 52 | string name; 53 | bool gender; 54 | double scores; 55 | }; 56 | // do something 57 | return 0; 58 | } 59 | ``` 60 | 61 | 逻辑上两种方式完全一样,只是第一种方式`student`类型可以被任何函数使用,但如果写在 main 函数内部,就只能在 main 函数内部使用。 62 | 63 | 结构体变量也可以定义在函数内部定义,定义在外部的变量被所有函数所共享,也就是全局变量。C++ primer 当中提倡尽量使用外部结构体。 64 | 65 | ### 初始化方式 66 | 67 | 我们可以和数组一样,在花括号内使用逗号进行分隔,如: 68 | 69 | ```C++ 70 | student xiaoming = {"xiaoming", 1, 3.5}; 71 | ``` 72 | 73 | 编译器会按照顺序,分别将"xiaoming"赋值给 name,1 赋值给 gender,3.5 赋值给 score。我们也可以使用 C++11 的列表初始化方式省略掉中间的等于号: 74 | 75 | ```C++ 76 | student xiaoming {"xiaoming", 1, 3.5}; 77 | ``` 78 | 79 | ### 结构体数组 80 | 81 | 定义了结构体之后,我们还可以像是基础变量类型一样定义结构体数组。 82 | 83 | ```C++ 84 | struct student { 85 | string name; 86 | bool gender; 87 | double scores; 88 | }; 89 | 90 | student sts[10]; 91 | ``` 92 | 93 | 对于结构体数组来说,我们也可以使用列表初始化方式来初始化,由于结构体本身的初始化就用到花括号,所以数组的初始化会使用到花括号的嵌套,像是这样: 94 | 95 | ```C++ 96 | student sts[2] = { 97 | {"xiaoming", 1, 3.6}, 98 | {"john", 1, 5.2} 99 | }; 100 | ``` 101 | -------------------------------------------------------------------------------- /C++/14-枚举类型.md: -------------------------------------------------------------------------------- 1 | ## 枚举 2 | 3 | ### 简介 4 | 5 | C++当中提供了枚举操作,我们可以使用`enum`关键字创建枚举类型。 6 | 7 | 这种方式创建的为符号常量,可以代替`const`关键字,并且还可以自定义名字,让代码可读性更强。如: 8 | 9 | ```C++ 10 | enum color {red, blue, orange, white, black}; 11 | ``` 12 | 13 | 在这一句语句当中完成两件事,首先我们创建了一个新的变量类型叫做`color`,这是一个枚举类型。其次我们创建了一些符号常量,例如`red`,`bule`,`orange`这些。在默认状态下,会将这些枚举量赋值为整数,第一个枚举量`red`为 0,第二个`blue`为 1,以此类推。 14 | 15 | 当然我们也可以显示地给这些枚举量赋值,如: 16 | 17 | ```C++ 18 | enum color {red=3, blue=1, orange, white, black}; 19 | ``` 20 | 21 | 这样前面给定了数值的`red`和`blue`会按照我们给定的值进行赋值,而之后从`orange`开始,依次赋值成 2,3,4. 22 | 23 | ### 使用 24 | 25 | 我们定义了枚举类型之后,可以当做正常类型来进行声明: 26 | 27 | ```C++ 28 | color a; 29 | ``` 30 | 31 | 由于`color`是一个枚举类型,所以当我们赋值的时候,只能赋值列举出来的类型,如果附上其他的值可能会出问题。根据编译器的不同出现的结果也不一样,有些编译器会报错,有些则只会给出警告。不管是报错还是警告,我们都不应该这么做: 32 | 33 | ```C++ 34 | color a = red; // OK 35 | color a = 10; // 报错或警告 36 | ``` 37 | 38 | 由于`enum`底层存储的是整型,所以有一些奇怪的操作是允许的,但是也强烈不建议这么做,也可能会有很大的风险。比如: 39 | 40 | ```C++ 41 | cout << (red < blue) << endl; // 比较大小 42 | cout << blue - red << endl; // 做加减法 43 | 44 | int c = red + 3; // 赋值给int 45 | ``` 46 | 47 | 这些操作在语法上都是允许的,但非常不推荐这么干,因为没有意义。枚举型当中每一个类型都有各自的逻辑含义,是不能拿来做计算的。虽然语法上可行,但逻辑上没有意义。 48 | 49 | 我们也可以使用强制转换将整型转成枚举类型: 50 | 51 | ```C++ 52 | color c = color(3); 53 | ``` 54 | 55 | 但同样不推荐,因为有可能数字 3 对应的枚举量并不存在,这也不会报错,但也许会影响程序的正确性。 56 | 57 | ### 枚举值的取值范围 58 | 59 | 前文说了,只有声明中的枚举值是有效的,然而由于 C++允许使用强制转换转换成枚举值,所以理论上枚举值取值范围内的值都可以被转换成枚举值,虽然这些值在逻辑上不一定有意义。 60 | 61 | 对于枚举变量来说,它的范围并不是固定的,而是根据定义情况波动的。C++会根据枚举值声明的情况计算上限和下限,只能允许在范围内的整型值强制转化为枚举值。 62 | 63 | ```C++ 64 | enum flag {black = 1, white = 2, red = 23}; 65 | ``` 66 | 67 | C++采取的是最小长度的方式,比如说我们上面定义的枚举值最大的是 23,它会计算出大于 23 的最小 2 的幂,也就是 32。所以这个枚举值的上限就是 31,对于下限也会采用类似的计算,如果定义的最小值大于等于 0,那么它的下限就是 0,否则采取同样的算法,只不过加上负号。 68 | 69 | 之所以会如此复杂,也是为了尽可能地节省内存空间。毕竟很多 C++程序的运行环境都是单片机或者是芯片,内存并不充裕。 70 | -------------------------------------------------------------------------------- /C++/15-指针初探.md: -------------------------------------------------------------------------------- 1 | ## 指针初探 2 | 3 | ### 前言 4 | 5 | C++可以说是成也指针、败也指针。依靠着指针,我们可以灵活地操控变量内存地址,实现很多独有的功能。但也正因为指针,尤其是使用不当的时候会产生许多的问题。导致许多工程师对于 C++以及指针深恶痛绝,以至于 C++之后的许多语言都摒弃了指针的设计,比如 Java 和 Python。 6 | 7 | 我们先把头疼的内容放一放,先从一些简单的概念开始。 8 | 9 | 首先要明确的是指针是一个变量,它特殊的点在于虽然同样是变量,它存储的并不是值,而是一个内存地址。内存地址顾名思义就是存放在内存当中的位置,对于非指针的变量, 我们也可以使用`&`操作符去获取它的地址。这就是为什么我们使用`scanf`在读取变量的时候,需要在变量名之前加上一个`&`符号。 10 | 11 | ```C++ 12 | int a; 13 | scanf("%d", &a); 14 | ``` 15 | 16 | 目的就是为了将 a 变量的地址传给`scanf`函数,从而将屏幕当中读取到的内容填写到 a 变量对应的地址当中。 17 | 18 | 我们也可以直接输出一个变量的地址,但输出结果是一个十六进制的数,代表一个内存位置。如果大家学过汇编或者是了解过底层的话,应该不陌生。这个输出的结果是给机器看的,人类无法读懂。 19 | 20 | ```C++ 21 | int a; 22 | cout << &a << endl; 23 | ``` 24 | 25 | ![](https://tva1.sinaimg.cn/large/008i3skNgy1gvtub2pkdfj3148030q37.jpg) 26 | 27 | ### 声明和初始化 28 | 29 | 指针和普通变量不同,它存储的值是地址。所以在声明指针的时候,也会有一点细小的区别。我们通过\*符号创建指针,\*运算符称为间接值(indirect value)或解除引用(dereferencing),现在理解这两个概念可能有些费劲,没关系我们可以先放一放。只许看记住使用\*创建指针即可,\*写在类型和变量名中间,如: 30 | 31 | ```C++ 32 | int * p; 33 | ``` 34 | 35 | 这样我们就创建了一个`int`型的指针,它的名字叫做 p。关于\*的位置,有些人喜欢紧跟着变量类型,有些人喜欢紧跟着变量名。其实都可以,看个人喜好。传统上来说 C 程序员喜欢后者,突出 ptr 是一个指针。 36 | 37 | ```C++ 38 | int *ptr; 39 | ``` 40 | 41 | C++程序员更喜欢前者,突出是一个`int`型的指针: 42 | 43 | ```C++ 44 | int* ptr; 45 | ``` 46 | 47 | 这两种都可以,对于编译器来说没有任何区别。但是要注意的是,每一个指针变量都需要一个\*: 48 | 49 | ```C++ 50 | int a, *ptr; 51 | ``` 52 | 53 | 前面说了,由于指针的值是一个地址,所以我们在对指针进行初始化或者赋值的时候,就需要用到取地址符。 54 | 55 | ```C++ 56 | int a = 3; 57 | int *p = &a; // 获取了a的地址 58 | ``` 59 | 60 | 当我们有了指针变量之后,我们可以使用\*来访问它指向的内存地址的值。 61 | 62 | ```C++ 63 | int a = 3; 64 | int *p = &a; 65 | 66 | cout << *p << endl; //output: 3 67 | ``` 68 | 69 | 要注意的是,由于指针 p 指向 a 的地址,所以当我们通过\*符号修改了 p 指向的值之后,a 的值一样会发生变化。 70 | 71 | ```C++ 72 | *p = 5; 73 | cout << a << endl; //output: 5 74 | ``` 75 | 76 | 正因为指针有这样的特性,所以使用的时候千万小心…… 77 | -------------------------------------------------------------------------------- /C++/16-指针初探(二).md: -------------------------------------------------------------------------------- 1 | ## 指针初探(二) 2 | 3 | ### 危险的 case 4 | 5 | 指针由于能够操作内存,所以如果使用的时候不够仔细,很容易引发一些意想不到的错误。 6 | 7 | C++ Primer 当中给了这样一个例子: 8 | 9 | ```C++ 10 | int *ptr; 11 | *ptr = 2333; 12 | ``` 13 | 14 | 在这段代码当中我们声明了一个`int`型的指针,并且将它指向了 2333。然而,这里有一个问题,我们在声明指针的时候并没有进行初始化。没有初始化的指针并不为空,而是指向一个未知的地方。如果说它指向的是一个常量 1200 的地址,我们让它等于 2333,那么之后当我们使用 1200 这个常量的时候,得到的结果都是 2333。 15 | 16 | 更可怕的是,整个过程非常地隐蔽,很难察觉。debug 的时候会令人抓狂。 17 | 18 | 所以千万不要修改一个没有初始化的指针指向的值。 19 | 20 | ### 指针和数字 21 | 22 | C++ Primer 当中还给了另外一个例子,当我们输出指针的时候,得到的是一串十六进制的数。那我们能不能反过来将一个十六进制的数赋值给指针呢? 23 | 24 | ```C++ 25 | int *p; 26 | p = 0xB8000000; 27 | ``` 28 | 29 | 答案是不行,因为类型不一致。虽然我们打印指针的时候看起来得到是十六进制数,但它的类型其实是指针类型,而不是整数类型。所以我们将一个整数赋值给一个指针是不行的,如果非要赋值,必须要进行类型转换。 30 | 31 | ```C++ 32 | int *p; 33 | p = (int*) 0xB8000000; 34 | ``` 35 | 36 | 但是这一转换之后显然又出现了一个问题,我们知道 0xB8000000 这个地址指向哪里么?显然不知道,自然也就说不清改了这里的值之后会引发什么结果。 37 | 38 | 所以虽然这么做可行,但也强烈不建议这样干。 39 | 40 | ### new 操作 41 | 42 | 前文说过使用指针有一个非常大的好处就是可以在程序运行的时候,动态分配内存。其实在 C 语言当中也有类似的功能,可以使用`malloc`来分配内存。不过在 C++当中有了更好用的运算符——`new`。 43 | 44 | 比如我们要动态创建一个`int`类型的变量,可以这样写: 45 | 46 | ```C++ 47 | int *ptr = new int; 48 | ``` 49 | 50 | `new`运算符根据之后的类型确定需要的内存大小,找到这样的内存之后,返回地址。刚好指针接收的值就是内存地址,因此刚好可以完成这样的赋值操作。 51 | 52 | 上面的代码也可以写成这样: 53 | 54 | ```C++ 55 | int a; 56 | int *ptr = &a; 57 | ``` 58 | 59 | 这两者有什么区别呢?表面上看没有区别,都是创建了一个`int`类型的变量。只不过第二种写法除了可以使用指针`ptr`之外,还可以使用变量名 a 来访问这个`int`。 60 | 61 | 但实际上这两者的内部实现完全不同,我们直接通过变量名创建的变量它的值会被存储在栈内存当中,而通过`new`创建的对象则被存储在堆内存当中。栈内存是由系统自动分配,而堆内存则是由程序员进行申请使用。这两者的内存模型是完全不同的,我们会在之后的文章详细地讨论这点。目前简单来理解的话,就是堆内存更加灵活,它的空间也更大,可以存储下更大的数据。 62 | 63 | ### delete 操作 64 | 65 | 有了动态创建,自然也就有动态删除,所以 C++当中有一个`delete`操作和`new`相对应。 66 | 67 | `delete`运算符可以在变量使用结束之后,将内存归还给内存池。因为很多时候程序当中的变量都是一次性使用或者是有生命周期的,当生命周期结束,使命完成就没有必要继续占用着资源了。毕竟系统内的内存资源是有限的,尤其是在一些大型项目或者嵌入式系统当中,内存资源非常紧张。 68 | 69 | `delete`运算符之后跟一个指针,它会释放改指针指向的内存。 70 | 71 | ```C++ 72 | int *ptr = new int; 73 | delete ptr; 74 | ``` 75 | 76 | 这里面有很多坑,千万要当心。首先是使用了`new`创建了内存之后,一定要记得`delete`,否则这块内存将会永远被占用无法得到释放,这种情况被称为内存泄漏(memory leak)。另外,我们不能`delete`一个已经`delete`过的指针,这也会引发严重错误。C++ Primer 对此的描述是:什么情况都可能发生。当然也不能再使用一个已经被`delete`的指针,这会引发空指针错误。 77 | 78 | 指针对于 C++来说是一把双刃剑,像是 Java、Python、Go 等其他语言,内存回收的工作都是由系统自动执行的。例如 Java 的 JVM 虚拟机设计了严密的 GC(垃圾回收)机制,程序员无须关心内存的回收问题,全部交给程序自动完成。 79 | 80 | 而在 C++当中,这一过程是由程序员手动执行的,某种程度上来说,这当然非常好,程序员拥有了很高的权限以及灵活度。但同样也是一个坑,尤其是在复杂系统当中,很难准确判断`delete`执行的时间。这会引发严重的问题,例如内存泄漏严重,野指针到处飞等…… 81 | -------------------------------------------------------------------------------- /C++/17-指针初探(三).md: -------------------------------------------------------------------------------- 1 | ## 指针初探(三) 2 | 3 | 我们前面使用指针创建的都是单个变量,在这种情况下,使用指针的优势并不明显。很多程序员仍然会选择使用声明变量的方式,而当我们需要动态创建数组这种大型数据的时候,指针就能体现出优势了。 4 | 5 | 我们使用声明的方式创建的数组在编译时就已经分配好了内存空间了,即使我们在程序当中完全不使用,它也依然存在占用了资源。这种编译时给数组分配内存被称为静态联编(static binding),意味着数组是在编译时加入程序的。 6 | 7 | 而使用 new 创建的数组则是在运行时,我们前文也说过,两者最大的区别在于一个是栈内存一个是堆内存。我们可以用程序去控制它是否创建,以及在什么情况下创建,并且数组的长度。因此这种数组被称为动态数组(dynamic array),这种创建方式称为动态联编(dynamic binding)。 8 | 9 | ### 创建数组 10 | 11 | 使用`new`创建数组很容易,和声明的方式一样,只需要将数组的类型以及元素数量写明即可。 12 | 13 | ```C++ 14 | int *arr = new int[10]; 15 | ``` 16 | 17 | `new`运算符会申请一块 10 个`int`大小的内存,并且将第一个元素的地址进行返回给指针`arr`。 18 | 19 | 对于使用`new`创建的数组,我们同样也可以使用`delete`来释放,但需要注意的是,由于我们申请的是数组类型,所以在`delete`的时候也需要写明这是一个数组: 20 | 21 | ```C++ 22 | delete [] arr; 23 | ``` 24 | 25 | 如果不加方括号,那么`delete`仅仅会释放指针指向的元素也就是数组的第一个位置。所以我们在使用`new`和`delete`的时候一定要注意,在创建时如果使用了方括号申请了数组,那么在`delete`的时候也需要加上方括号。如果不匹配则可能会引发不确定的后果,所以千万要当心。 26 | 27 | C++ Primer 当中整理了几条规则,搬运过来: 28 | 29 | - 不要使用`delete`释放不是`new`分配的内存 30 | - 不要使用`delete`释放同一块内存两次 31 | - 如果使用`new[]`为数组分配内存,则也应该使用`delete[]`来释放 32 | - 如果使用`new`为非数组分配内存,应该使用`delete`来释放 33 | - 对空指针`delete`是安全的 34 | 35 | ### 使用动态数组 36 | 37 | 聊完了`delete`的用法, 我们再来看看动态数组的使用。 38 | 39 | 这里有一个问题在于,我们通过`new`创建得到的虽然是一个数组,但是却是以指针的形式得到的,那么我们怎么来使用呢? 40 | 41 | 其实很简单,我们就当做和数组一样来使用就行了: 42 | 43 | ```C++ 44 | int *p = new int[10]; 45 | 46 | p[2] = 5; 47 | cout << p[2] << endl; 48 | ``` 49 | 50 | 因为数组本质上就是常量指针,因此它们基本等价。唯一的不同在于数组是常量指针,所以我们不能对数组名进行赋值操作,而指针可以: 51 | 52 | ```C++ 53 | p = p + 1; 54 | ++p; 55 | ``` 56 | 57 | 是的,指针可以进行算术操作,对于指针进行加减操作其实等价于指针的移动。比如指针加一代表指向的元素向后移动一位。 58 | 59 | 原本 p 指针指向数组的下标 0 的位置,当 p++之后,指向 1 的位置。因为数组是一块连续的内存,不同类型的变量的长度不同。因此在底层运算的时候,指针指向的位置移动的长度其实并不是 1,而是一个变量类型占用的字节数。 60 | 61 | 同理,指针也可以做减法,得到的差是一个整数。这种运算只有两个指针指向同一个数组才有意义,代表两个指针之间间隔的元素数量。 62 | -------------------------------------------------------------------------------- /C++/18-内存模型初探.md: -------------------------------------------------------------------------------- 1 | ## C++内存模型简介 2 | 3 | ### 动态创建结构体 4 | 5 | 我们之前介绍了使用`new`运算符来动态创建数组的相关用法,`new`操作符除了可以动态创建数组之外,也可以用来动态创建结构体、类对象。同样和通过声明的方式不同,动态创建的方式创建的内存在堆内存当中,更加的灵活。 6 | 7 | 假设我们定义了这么一个结构体: 8 | 9 | ```C++ 10 | struct P { 11 | int x, y; 12 | }; 13 | ``` 14 | 15 | 我们可以如下创建一个它的对象: 16 | 17 | ```C++ 18 | P *p = new P; 19 | ``` 20 | 21 | 但创建了之后有一个问题,我们怎么样访问结构体当中的成员变量呢?我们知道如果是通过声明的方法创建的对象,我们可以通过`.`运算符来访问: 22 | 23 | ```C++ 24 | P p; 25 | p.x = 3; 26 | ``` 27 | 28 | 对于指针,我们有一个全新的运算符写成`->`,也就是箭头符号。我们可以通过箭头符号来访问一个指针指向的结构体当中的元素: 29 | 30 | ```C++ 31 | P *p = new P; 32 | p->x = 3; 33 | ``` 34 | 35 | 当然我们也可以对指针进行解引用,这样得到的就是一个结构体实例,我们可以直接用`.`来访问。不过这样没什么必要,尤其是新手搞不好会被搞晕。所以如果是萌新,就记住指针用箭头符号,非指针用`.`即可。 36 | 37 | ```C++ 38 | P *p = new P; 39 | // p解引用就得到了实例,可以使用. 40 | (*p).x = 3; 41 | ``` 42 | 43 | ## 内存分配原理 44 | 45 | C++当中的内存分配主要分为三种方式,分别是自动存储、静态存储和动态存储,C++11 当中新增了线程存储,我们将在之后讨论。 46 | 47 | ### 自动存储 48 | 49 | 自动存储可以理解成局部变量,指的是在函数内部定义的常规变量,也被称为自动变量。它们会在函数调用的时候自动创建,在函数结束的时候消亡。 50 | 51 | ```C++ 52 | void test() { 53 | int x; 54 | return ; 55 | } 56 | ``` 57 | 58 | 比如我们在`test`函数当中创建了一个`int`型的变量 x,当函数调用结束时,这个变量内存会被释放。 59 | 60 | 自动变量的作用范围为包含它的代码块,也就是花括号所包括的那一段代码,其实并不止包含函数,循环、分支语句等也包含在内。 61 | 62 | 自动变量通常存储在栈当中,当我们执行代码的时候,其中的变量依次加入到栈中。而在代码块执行完成离开的时候,按照相反的顺序依次释放。如果大家学过操作系统的话,会了解这种内存模型称为后进先出(LIFO)。 63 | 64 | ### 静态存储 65 | 66 | 静态存储指的是在整个程序执行期间都存在的存储方式。使变量称为静态的方式有两种,一种是定义在函数之外,成为全局变量。另外一种是在声明变量的时候使用关键字`static`。 67 | 68 | ```C++ 69 | static int a = 30; 70 | ``` 71 | 72 | 简单理解来说,`static`关键字修饰的变量只会初始化一次,即使函数反复调用。有的时候我们希望函数多次调用的时候某些中间结果可以被保存,一般来说想要实现这样的功能需要定义全局变量。然而全局变量的控制域就不再是函数本身,如果我们希望变量的作用域依然限制在函数本身,就可以使用`static`关键字。 73 | 74 | 来看一个简单的例子: 75 | 76 | ```C++ 77 | void fn() { 78 | static int n = 10; 79 | cout << n << endl; 80 | n++; 81 | } 82 | 83 | 84 | int main() { 85 | fn(); 86 | fn(); 87 | fn(); 88 | } 89 | ``` 90 | 91 | 我们定义了一个函数`fn`,在`fn`当中我们定义了一个静态变量 n,然后每次都执行了 n++的操作。如果把 n 理解成函数内部的动态变量,那么我们每次调用`fn`得到的结果都应该是 10,因为每次调用都会初始化。由于我们加上了`static`关键字,尽管我们在`main`函数当中调用了三次,但只会初始化一次。所以最终得到的结果是: 92 | 93 | ![](https://tva1.sinaimg.cn/large/008i3skNgy1gvx358rwikj30zy03y74j.jpg) 94 | 95 | ### 动态存储 96 | 97 | 动态存储就是使用`new`创建的变量,C++当中为此管理了一个内存池,叫做自由存储空间(free store)或(堆)。堆内存和前面说的自动存储和静态存储是分开的,使用`new`和`delete`函数,可以使我们可以在一个函数当中分配内存,并且在另外一个函数当中释放它。因此数据的生命周期不完全收到程序或者函数的限制。 98 | 99 | 另外有一点要说明的是,在栈中,由于自动添加和自动删除机制。所以其中的内存是连续的,而在堆当中则不然,`new`和`delete`的使用会导致内存分片不连续,可能会导致在分配新内存的时候更加困难,尤其是当我们申请的内存比较大时。 100 | -------------------------------------------------------------------------------- /C++/19-for循环.md: -------------------------------------------------------------------------------- 1 | ## for 循环 2 | 3 | 在编程当中我们经常会遇到需要重复执行的步骤,想要让计算机能够重复执行某些逻辑,就需要使用循环。 4 | 5 | 在 C++当中,循环有三种类型,分别是`for`循环、`while`循环和`do while`循环。其中前两者使用较多,最后一个很少使用。 6 | 7 | ### 基础用法 8 | 9 | 求 1 + 2 + 3 + ... + 100 的和,使用 for 循环实现: 10 | 11 | ```C++ 12 | int ret = 0; 13 | int i; 14 | for (i = 1; i <= 100; i++) { 15 | ret += i; 16 | } 17 | ``` 18 | 19 | 这里的`i++`表示自增操作,是`i+=1`的简写,也可以写成`++i`。不过`++i`和`i++`并不能完全混用,我们将会在之后的内容当中进行阐述。 20 | 21 | 根据 C++ Primer 当中的定义,`for`循环可以分成三个部分。第一个部分是初始化,在这个`for`循环当中,我们把循环因子 i 初始化成了 1。第二个部分是判断条件,也就是`for`循环的执行条件,在什么情况下需要执行循环。第三个部分是更新循环因子,比如在这个例子当中,我们当前的 i 加入了 ret 之后,自增 1 变成了下一个将要累加的数。最后一个部分就是花括号当中的执行体,也就是每次循环需要执行的内容。 22 | 23 | 写成通用形式就是: 24 | 25 | ```C++ 26 | for (initialization; test-expression; update-expression) { 27 | body; 28 | } 29 | ``` 30 | 31 | 初始化、判断条件以及更新因子这三个部分使用分号分隔。如果循环要执行的内容只包含一条语句,花括号也可以不写。不过为了保证全局的代码风格统一,最好还是统一使用花括号。 32 | 33 | `test-expression`决定循环是否结束,因此,在每一次 body 开始执行之前,都需要进行一次`test-expression`的判断。当`test-expression`为 true 时,才会执行 body 中的语句。 34 | 35 | `test-expression`并不一定需要是一个`bool`值,C++会进行强制转换。比如对于 int 类型,0 值会被转换为 false,而其他值都会转换成 true。如: 36 | 37 | ```C++ 38 | int ret = 0; 39 | int i; 40 | for (i = 100; i; i--) { 41 | ret += i; 42 | } 43 | ``` 44 | 45 | 这段逻辑和上面一样,同样是计算了 1 到 100 累加的值。只不过这里我们进行的是倒序相加,循环的执行条件为`i >= 0`。当 i=0 时结束,我们的判断条件可以写成 i。`int`型的 i 会被强制转换成`bool`型,当 i=0 时,值为 false,循环结束。其中自减符的用法和自增类似。 46 | 47 | ### 进阶用法 48 | 49 | 首先,我们可以将循环因子的声明写入 for 循环当中: 50 | 51 | ```C++ 52 | // version1 53 | int i; 54 | for (i = 0; i < 100; i++) ret += i; 55 | 56 | // version2 57 | for (int i = 0; i < 100; i++) ret += i; 58 | ``` 59 | 60 | 这样有两个好处,第一是简化了代码,将 i 的声明语句写入了`for`循环当中,可读性也更好。另一点是限定了 i 这个变量的使用范围,在 for 循环当中声明的变量,**它的作用域也只有`for`循环**,出了循环之后,i 这个变量将消失。 61 | 62 | 不过在一些老旧的编译器(如 VC6.0)当中并不会这样,循环当中声明的变量依然会继续存在。 63 | 64 | 另外`initialization; test-expression; update-expression`这三条语句都不是必须的,可以根据我们的需要进行省略。 65 | 66 | 比如 for 循环所需要的初始化工作之前已经完成,那么就可以省略`initialization`: 67 | 68 | ```C++ 69 | int i = 0; 70 | for (; i < 100; i++) ret += i; 71 | ``` 72 | 73 | 比如我们也可以将循环的结束条件放在 for 循环的 body 当中,如: 74 | 75 | ```C++ 76 | for (int i = 0;; i++) { 77 | if (i > 100) break; 78 | ret += i; 79 | } 80 | ``` 81 | 82 | 我们的更新条件同样也可以放在 body 中: 83 | 84 | ```C++ 85 | for (int i = 0; i < 100;) { 86 | ret += i; 87 | i++; 88 | } 89 | ``` 90 | 91 | 甚至,我们可以极端一点,三个条件都省略: 92 | 93 | ```C++ 94 | int i = 0; 95 | for (;;) { 96 | if (i > 100) break; 97 | ret += i; 98 | i++; 99 | } 100 | ``` 101 | 102 | 同样,我们更新的条件也不一定只能自增或自减,其他的任何逻辑也都可以。正因此,C++当中的`for`循环是非常灵活的, 使用得当完全可以代替`while`循环和`do while`循环。 103 | 104 | 不过除非必要,我们还是要尽量遵守代码规范,不要省略条件,这样代码可读性也会更好一些。 105 | -------------------------------------------------------------------------------- /C++/2-常用语句.md: -------------------------------------------------------------------------------- 1 | ### 声明变量 2 | 3 | 在 C++当中所有的变量都需要声明,如: 4 | 5 | ```C++ 6 | int wordCnt; 7 | ``` 8 | 9 | 我们声明了一个 int 类型的变量 wordCnt,这样的语句会告诉编译器两个关键信息。一个是变量所需要的内存,一个是这块内存的名称。比如在这个例子当中,我们声明了一个 int 型的变量。它占据 32 个二进制位,也就是 4 个字节,这块内存的名称被叫做 wordCnt。 10 | 11 | 注:在有些语言(如 basic)当中变量无须声明,可直接使用。但这会引起部分问题,如拼写错误时很难检查。 12 | 13 | 对于变量声明,C++ Primer 推荐尽可能在首次使用变量之前就声明它。 14 | 15 | ### 赋值语句 16 | 17 | 变量被声明了之后,我们就可以通过赋值语句给它赋上我们想要的值。 18 | 19 | 例如: 20 | 21 | ```C++ 22 | wordCnt = 10; 23 | ``` 24 | 25 | C++当中支持连续赋值的写法,例如: 26 | 27 | ```C++ 28 | int wordCnt; 29 | int personCnt; 30 | int roomCnt; 31 | wordCnt = personCnt = roomCnt = 10; 32 | ``` 33 | 34 | 这就是一个连续赋值的操作,10 先赋值给最右侧的 roomCnt,再赋给 personCnt,最后赋给 wordCnt。 35 | 36 | ### cin、cout 语句 37 | 38 | cin、cout 同样是 C++当中常用的语句。 39 | 40 | cin 顾名思义,表示读入,它可以从屏幕(终端)读入数据,流向我们指定的变量。例如: 41 | 42 | ```C++ 43 | int wordCnt; 44 | cin >> wordCnt; 45 | ``` 46 | 47 | cin 是输入数据的对象,数据从 cin 流向了 wordCnt。即我们在终端输入的数据被读入到了 wordCnt 当中。 48 | 49 | 和 cout 一样,我们可以从终端读入多种类型的数据,如浮点数、整数、字符串等,cin 会自动将读入的数据转化成对应的数据类型并完成赋值。 50 | 51 | 我们使用 cout 输出结果时可以通过多个<<符号进行拼接,如: 52 | 53 | ```C++ 54 | cout << "word count: " << wordCnt << "room count: " << roomCnt << endl; 55 | ``` 56 | 57 | ### 库函数 58 | 59 | C++官方提供了许多库函数,这些函数的实现往往分布在不同的头文件当中。我们需要首先 include 对应的头文件才能进行使用。 60 | 61 | 例如计算平方根的函数 sqrt 的实现在 cmath 库中,我们需要首先 include cmath 这个库,才能使用它。 62 | 63 | ```C++ 64 | #include 65 | using namespace std; 66 | 67 | double a = sqrt(10.0); 68 | ``` 69 | 70 | 对于库函数我们需要首先查找到它对应的头文件,将其 include 之后再进行使用。 71 | 72 | ### 自定义函数 73 | 74 | C++当中函数同样分为声明和实现,函数的声明一定要写在 main 函数之前,否则 main 函数在调用的时候将会找不到对应的函数,报错`error: use of undeclared identifier`。 75 | 76 | 所以一种正确的写法是在 main 函数之前写上函数的声明,函数的实现写在 main 函数之后。其实只需要保证函数声明在 main 函数之前即可,函数的实现并不限制摆放位置。 77 | 78 | 对于函数的声明,和变量的声明类似,它分为三个部分。分别是函数返回类型,函数名和函数所需的外界参数。例如: 79 | 80 | ```C++ 81 | void test(); 82 | int getValue(int x, int y); 83 | ``` 84 | 85 | 上面所写的都是函数的声明,如果函数无需外界参数,也需要保留小括号。 86 | 87 | 另外在函数的声明当中,变量名也可以省略,只需要标注类型即可,所以 getValue 的函数声明又可以写成: 88 | 89 | ```C++ 90 | int getValue(int, int); 91 | ``` 92 | 93 | 如果怕麻烦,可以将函数的声明和实现写在一起,放在 main 函数之前即可。 94 | 95 | 例如: 96 | 97 | ```C++ 98 | int getValue(int x, int y) { 99 | return x + y; 100 | } 101 | 102 | int main() { 103 | cout << getValue(3, 5) << endl; 104 | return 0; 105 | } 106 | ``` 107 | -------------------------------------------------------------------------------- /C++/20-自增与自减.md: -------------------------------------------------------------------------------- 1 | ## 自增与自减 2 | 3 | ### 基本用法 4 | 5 | 自增与自减是 C++当中两个使用频率非常高的运算符,不仅在循环当中用到,在日常的代码当中也经常使用。 6 | 7 | 甚至 C++这个名称的由来都和自增运算符有关,表示 C 语言的升级版。当然这也是 C#名字的由来,#这个符号表示 4 个叠加的加号……不得不吐槽这微软的恶趣味。 8 | 9 | 我们都知道自增有两种写法,一种是`i++`另外一种是`++i`。这两种写法对于 i 这个变量的最终结果来说是一样的,都是自增了 1,但是对于自增这个操作的发生时间,则有很大的差异。 10 | 11 | 比如: 12 | 13 | ```C++ 14 | int a = 0, b = 0; 15 | cout << a++ << endl; 16 | cout << ++b << endl; 17 | ``` 18 | 19 | 最终我们得到的输出结果是 0 和 1,差别就在执行自增的时间。对于`cout << a++`来说,它是先执行 cout 操作,再执行自增,而`cout << ++b`则相反,是先执行自增再执行 cout。 20 | 21 | 同理,我们在赋值的时候也是一样: 22 | 23 | ```C++ 24 | int a = 0, b = 0; 25 | int c = a++; 26 | int d = ++b; 27 | ``` 28 | 29 | c 和 d 得到的结果同样是一个为 0,另外一个为 1,原因和刚才一样。 30 | 31 | 以上的规则同样适用于自减。 32 | 33 | ### 进阶理解 34 | 35 | 现在我们知道了`++i`的执行顺序在`i++`之前,那么问题来了,那么它们两者的执行顺序究竟是怎样的?差异到底在哪里呢? 36 | 37 | 对此,C++当中有一个叫做顺序点的概念,顺序点指的是程序执行过程中的一个点。在 C++当中语句中的分号就是一个顺序点,在程序处理下一条语句之前,赋值运算符、自增、自减运算符执行的所有修改都必须完成。除了分号之外,完整的表达式末尾也是一个顺序点。 38 | 39 | 完整表达式的概念有点费解,C++ Primer 中的定义是不是另一个更大的表达式的子表达式,比如`while`循环中的检测语句就是一个完整表达式。 40 | 41 | 比如: 42 | 43 | ```C++ 44 | int cnt = 0; 45 | while (cnt++ < 10) cout << cnt << endl; 46 | ``` 47 | 48 | 程序的输出结果是: 49 | 50 | ![](https://tva1.sinaimg.cn/large/008i3skNgy1gvzchn3rh6j316y0auwex.jpg) 51 | 52 | 我们可以看到它的输出结果从 1 开始,而并非从 0 开始。意味着我们在执行 cout 之前,cnt 变量就已经完成了自增。这进一步说明了`while(cnt++ < 10)`本身就已经是一个完整表达式了。因此在这个表达式执行之前,C++就会完成自增的操作。 53 | 54 | 关于完整表达式还有一个坑点,就是它的执行顺序。比如下面这个例子: 55 | 56 | ```C++ 57 | y = (4 + x++) * (6 + x++); 58 | ``` 59 | 60 | 由于`(4 + x++)`和`(6 + x++)`都不是一个完整表达式,因此 C++并不能保证`x++`的执行顺序,它没有规定是在每个子表达式计算之后执行自增,还是整个表达式计算之后再自增。它只能保证在执行到下一条语句之前`x`变量被自增两次,至于它的执行时间则无法保障。 61 | 62 | 因此,最好不要写出这样的代码,不仅可读性差,而且结果也可能不可靠。 63 | 64 | ### 差异 65 | 66 | 我们还有一个问题没有解决,在不影响结果的情况下,前缀的形式和后缀的形式究竟还有没有其他差别呢? 67 | 68 | 比如: 69 | 70 | ```C++ 71 | x++; 72 | ++x; 73 | 74 | for (int i = 0; i < n; i++); 75 | for (int i = 0; i < n; ++i); 76 | ``` 77 | 78 | 我们现在知道它们的结果是一样的,但在内部执行是有细微差别的。差别在于后缀的形式会先生成一个拷贝值,再将拷贝值赋值给原值,而前缀的版本是直接在原值上修改。因此理论上来说,前缀版本的效率更高。当然这当中的差别非常细微,几乎可以忽略不计。 79 | 80 | 但是在面试当中很有可能会被问到,因此有所了解即可。 81 | 82 | ### 指针自增、自减 83 | 84 | 自增自减操作同样可以运用在指针上,前文当中介绍过,这表示指针的移动。自增表示向右移动一位,自减表示向左移动一位。 85 | 86 | 这很简单,但是当我们把一些操作符结合在一起就有些麻烦了。C++当中规定,前缀运算符和解引用运算符优先级相同,按照从右到左的方式结合,后缀运算符优先级更高,从左到右。 87 | 88 | 这意味着`*++pt`表示先执行指针自增操作,也就是移动一位之后,再解引用。 89 | 90 | `++*pt`则意味着先解引用取得值,再对改值加 1。 91 | 92 | `x=*pt++`由于后缀符的优先级更高,意味着先执行指针移动,再解引用。如果大家实在搞不清楚的话,可以使用括号。 93 | -------------------------------------------------------------------------------- /C++/21-while与do while循环.md: -------------------------------------------------------------------------------- 1 | ## while 循环 2 | 3 | `while`循环是没有条件初始化也没有条件更新的循环,它只有测试条件以及循环体。可以理解成类似这样的`for`循环: 4 | 5 | ```C++ 6 | for (; i < n;) { 7 | 8 | } 9 | ``` 10 | 11 | `while`循环写成这样: 12 | 13 | ```C++ 14 | while (test-condition) { 15 | body; 16 | } 17 | ``` 18 | 19 | 圆括号当中是测试条件,当测试条件为`true`的时候,循环执行,为`false`时退出。 20 | 21 | 很显然,如果我们在循环体当中不对条件的变量进行改动的话,这个循环会无限执行,根本不可能退出。比如下列代码: 22 | 23 | ```C++ 24 | int i = 0; 25 | while (i < 5) { 26 | cout << "hello" << endl; 27 | } 28 | ``` 29 | 30 | 因为我们没有在循环体当中对 i 的值进行修改,所以它永远也不可能满足`>=5`的退出条件,因此这个循环会无限运行。这样无限运行的循环成为死循环。 31 | 32 | 所以为了循环能正常退出,我们一定要记得在循环体当中加入确保循环会结束的逻辑。 33 | 34 | ## do while 循环 35 | 36 | `do while`循环和`for`循环以及`while`循环不同,它是出口条件,而非入口条件。 37 | 38 | 什么意思呢,也就是说`do while`循环是先执行循环体当中的内容, 再进行判断是否终止。而`for`循环以及`while`循环是先执行条件判断,满足条件再执行循环体。也就是说`do while`循环可以确保循环体至少运行一次。 39 | 40 | ```C++ 41 | do { 42 | body; 43 | } while (test-condition); 44 | ``` 45 | 46 | ### 基于范围的 for 循环(C++11) 47 | 48 | 在 C++11 当中新增了一种特性,可以基于范围进行`for`循环,有些类似于 Python 当中的循环操作。例如: 49 | 50 | ```C++ 51 | double prices[5] = {1.0, 2.3, 3.5, 6.1, 2.1}; 52 | 53 | for (double x: prices) { 54 | cout << x << endl; 55 | } 56 | ``` 57 | 58 | 相当于我们用一个变量 x 去遍历`prices`数组当中的所有元素。当然这样拿到的 x 是一个拷贝,相当于: 59 | 60 | ```C++ 61 | for (int i = 0; i < 5; i++) { 62 | double x = prices[i]; 63 | } 64 | ``` 65 | 66 | 我们直接去修改这里的 x 是不会生效的,因为它只是一个拷贝,想要对数组当中的元素进行修改的话,可以在 x 之前加上一个取地址符,将它设置成引用: 67 | 68 | ```C++ 69 | for (double &x : prices) { 70 | x = 5.0; 71 | } 72 | ``` 73 | 74 | 这样当我们对 x 修改的时候,就相当于对数组当中的元素进行修改了。这里只是简述这种循环方式,关于其中的一些技术细节,将会在之后的文章当中进行讨论。 75 | -------------------------------------------------------------------------------- /C++/22-二维与多维数组.md: -------------------------------------------------------------------------------- 1 | ## 二维与多维数组 2 | 3 | ### 声明与使用 4 | 5 | 在我们之前的文章当中,提到的数组都是一维的,也就是一行数据。 6 | 7 | 但有的时候,我们想要存储的数据往往是高维的。比如一张表格,比如一个矩阵等等。这个时候我们就需要用到二维或是多维数组了。虽然说是多维数组,但严格说起来,本质上还是一位数组的多重嵌套。 8 | 9 | 多维数组的定义和一维差别不大,只需要标记清楚每一个维度的大小即可: 10 | 11 | ```C++ 12 | int matrix[100][100]; 13 | ``` 14 | 15 | 这表明我们申请了一个 100 x 100 的二维数组,我们访问数组元素的方式也依然一样,通过方括号表明想要访问的下标即可: 16 | 17 | ```C++ 18 | cout << matrix[10][10] << endl; 19 | ``` 20 | 21 | 同理,更多维度也是一样的方式操作: 22 | 23 | ```C++ 24 | int mt[100][100][100]; 25 | 26 | cout << mt[10][10][10] << endl; 27 | ``` 28 | 29 | ### 初始化 30 | 31 | 二维数组的初始化也和一维数组类似,只不过同样由于维度的增加,我们需要增加一重花括号的嵌套: 32 | 33 | ```C++ 34 | int matrix[2][5] = { 35 | {0, 1, 2, 3, 4}, 36 | {1, 2, 3, 4, 5} 37 | }; 38 | ``` 39 | 40 | ### 多重 for 循环 41 | 42 | 我们要使用二维或者多维数组的时候,应该怎么样去遍历它当中的每一个元素呢? 43 | 44 | 显然我们不可能全靠手动编写,其实方法也很简单,我们只需要把`for`循环也嵌套起来,成为多重循环,就可以访问了。 45 | 46 | 例如: 47 | 48 | ```C++ 49 | int matrix[10][10]; 50 | 51 | for (int i = 0; i < 10; i++) { 52 | for (int j = 0; j < 10; j++) { 53 | cin >> matrix[i][j]; 54 | } 55 | } 56 | ``` 57 | 58 | 我们来简单地剖析一下代码,我们将两种循环叠加在了一起。对于最外层的循环来说,内部的`for`循环代码会被视为一整块。也就是说当内部的 j 完成从 0 到 10 完整地遍历一遍之后,对于外层的循环 i 来说,才算是完成了一次执行。所以这就意味着,i 每变化一次,j 都需要完成一整个遍历。 59 | 60 | 所以对于双重循环来说,它的执行复杂度是$O(nm)$,其中 n 和 m 分别是两重循环的长度。 61 | 62 | 同理,如果需要访问更多维度的数组,我们可以嵌套更多层循环。 63 | -------------------------------------------------------------------------------- /C++/23-if语句.md: -------------------------------------------------------------------------------- 1 | ## if 语句 2 | 3 | ### 纯 if 4 | 5 | 我们可以使用`if`语句来进行判断是否需要执行某一段逻辑。`if`有两种形式,一种是单纯的判断`if`语句,另外一种是加上否则条件的`if else`语句。 6 | 7 | 首先来看看第一种,单纯的`if`语句,它用来执行单独的判断。 8 | 9 | 它理解成自然语言就是:如果某一种情况发生,则执行……`if`语句之后跟的是一个`bool`型的判断条件,当为`ture`时执行语句块,否则不执行。 10 | 11 | ```C++ 12 | if (test-condition) { 13 | // do something 14 | } 15 | ``` 16 | 17 | `test-condition`可以接任何表达式,只要它的结果是`bool`型,甚至不是`bool`也依然可以,C++会进行强制转化: 18 | 19 | ```C++ 20 | if (1) { 21 | // do something 22 | } 23 | ``` 24 | 25 | 不过由于涉及强制转化还是需要小心,可能会有超出预期的结果出现,所以最好不要依赖类型转换,写清楚判断条件。 26 | 27 | ### if-else 28 | 29 | `if-else`语句和纯`if`语句几乎完全一样,只不过多了一个`else`关键字,它表示否则。也就是当条件不成立时执行的内容。 30 | 31 | ```C++ 32 | if (3 > 5) { 33 | cout << "it's wrong" << endl; 34 | }else { 35 | cout << "else works" << endl; 36 | } 37 | ``` 38 | 39 | 由于`3 > 5`不成立,所以我们不会执行`if`语句之后的代码块,并且由于我们使用了`else`关键字,所以会执行`else`之后的内容。 40 | 41 | 并且`if-else`语句可以嵌套,只要我们愿意几乎可以无限嵌套下去。都市传说某些游戏当中的逻辑由于过于复杂,嵌套了上万个`if-else`语句…… 42 | 43 | ```C++ 44 | if (condition1) { 45 | // todo 46 | }else if (condition2) { 47 | // todo 48 | }else { 49 | // todo 50 | } 51 | ``` 52 | 53 | 需要注意当多个`if-else`语句嵌套时,编译器会从上到下依次轮询,它的判断是有顺序的。如果多个条件同时为`true`,只会执行最上方的代码块。 54 | -------------------------------------------------------------------------------- /C++/24-逻辑表达式.md: -------------------------------------------------------------------------------- 1 | ## 逻辑表达式 2 | 3 | 我们无论是在`for`循环还是`while`循环或者是`if`条件判断当中,都用到了逻辑判断。 4 | 5 | 我们之前举的例子都非常简单,都是单个的判断。有时候我们的逻辑非常复杂,判断的条件有多个,这个时候就需要使用逻辑表达式了。 6 | 7 | 逻辑表达式由多个逻辑运算符连接在一起,逻辑运算符分别有 OR, AND 和 NOT,翻译过来就是与或非。 8 | 9 | ### OR 运算符 10 | 11 | or 运算符翻译过来是“或”的意思,表示两者或多者当中,有一个为`true`时结果为`true`,写作`||`,注意这里有两个竖线,单个竖线也是或运算,不过是位运算当中的或运算,表示的含义不同,千万不要搞错。 12 | 13 | 使用或运算符我们可以将多个判断条件并列在一起,只要有一个为`true`,最终的结果就是`true`。 14 | 15 | ```C++ 16 | 5 == 5 || 4 < 4; 17 | 5 > 3 || 2 < 1 || 4 < 7; 18 | ``` 19 | 20 | 这里`||`运算符的计算优先级比比较运算符低,所以这里可以不用括号。另外 C++当中规定`||`运算符也是一个顺序点,意味着编译器会计算左侧的值,再计算右侧的值。只要遇见表达式的结果为`true`则停止,不会再继续往右计算。 21 | 22 | ### AND 运算符 23 | 24 | and 运算符翻译过来是“与”的意思,表示两个条件同时满足,即两者皆为`true`时结果为`true`,写作`&&`。 25 | 26 | ```C++ 27 | 5 == 5 && 4 == 4; 28 | 4 == 3 && 1 < 2; 29 | ``` 30 | 31 | 同样,`&&`运算符的优先级也小于比较运算符。另外,`&&`运算符也是顺序点。意味着 C++先执行左侧结果,再执行右侧。如果左侧结果为`false`,那么右侧的结果将不会再执行。这个特性非常有用,特别是当我们使用一些可能会非法的元素的时候,例如: 32 | 33 | ```C++ 34 | string s; 35 | if (n < s.size() && s[n] == 'h') { 36 | // do something 37 | } 38 | ``` 39 | 40 | 在上面这个例子当中,我们需要判断`s[n]`这个元素,但是由于 n 可能很大超过字符串的范围。所以在使用之前需要先进行判断,如果`n < s.size()`不成立,那么`s[n]==h`的判断将不会执行,也就不会引发报错了。 41 | 42 | C++当中还给了一个 case,当我们用`&&`运算符来判断范围的时候,千万不要进行简写: 43 | 44 | ```C++ 45 | if (a >= 5 && a < 10); // 合法 46 | if (5 <= a < 10); // 非法 47 | ``` 48 | 49 | 因为后者在编译器当中表示不同的含义,会被翻译成`(5 <= a) < 10`,也就是先判断`5 <= a`,拿这个结果再去和 10 进行判断。显然`5 <= a`的结果是一个`bool`值,它一定是小于 10 的,那么无论`a`等于多少,这个表达式永远为`true`。 50 | 51 | ### NOT 运算符 52 | 53 | not 运算符也就是非运算符,表示对一个逻辑表达式的结果取反。`true`变成`false`,`false`变成`true`,写作`!`。例如: 54 | 55 | ```C++ 56 | if (!(x > 5)); 57 | ``` 58 | 59 | and 和 or 运算符的优先级都低于比较运算符,但 not 运算符不然,它的优先级高于所有的关系运算符和算术运算符。所以如果我们取反的对象是一个表达式,一定要记得加上括号。 60 | 61 | 比如: 62 | 63 | ```C++ 64 | !(x > 5); 65 | !x > 5; 66 | ``` 67 | 68 | 后者会先对 x 计算取反的操作,得到的结果为`true`或`false`,无论是哪个值它都显然小于 5。 69 | 70 | 另外,not 运算符的优先级高于 and 高于 or。因此表达式: 71 | 72 | ```C++ 73 | age > 30 && age < 45 && !flag || weight > 300 74 | ``` 75 | 76 | 会被解释成: 77 | 78 | ```C++ 79 | (age > 30 && age < 45 && (!flag)) || weight > 300 80 | ``` 81 | -------------------------------------------------------------------------------- /C++/25-三元表达式.md: -------------------------------------------------------------------------------- 1 | ## 三元表达式 2 | 3 | `if-else`语句非常常用,但在进行一些简单逻辑判断的时候,会显得有些不太简洁。特别是在初始化的时候,比如我们有一个变量,某种情况下赋值成 a,另外的情况下赋值成 b。 4 | 5 | 使用`if-else`语句写出来就是: 6 | 7 | ```C++ 8 | int cur; 9 | if (condition) { 10 | cur = a; 11 | }else { 12 | cur = b; 13 | } 14 | ``` 15 | 16 | 这当然是没问题的,只是在大量使用的时候会显得有些繁琐。因此 C++当中推出了三元表达式对此进行简化,三元表达式也被称为条件运算符(`?:`)。运算符的通用格式如下: 17 | 18 | ```C++ 19 | condition ? expression1 : expression2; 20 | ``` 21 | 22 | 问号之前的 condition 表示一个逻辑运算,如果结果为`true`,返回 expression1 的值,否则返回 expression2 的值。 23 | 24 | 使用三元表达式之后,刚才上述的代码可以改写成: 25 | 26 | ```C++ 27 | int cur = condition ? a : b; 28 | ``` 29 | 30 | 这样我们就把 6 行代码压缩成了一行,简化了代码,但也因此了增加了代码阅读的难度。因此只推荐在简单逻辑判断下使用三元表达式,也不推荐嵌套使用,会使得代码非常难以阅读。 31 | 32 | 最后分享一个我个人特别喜欢的使用三元表达式的场景,就是结构体排序的`cmp`函数。 33 | 34 | 比如我们有这样一个场景,需要使用一个结构体存储两个值 x 和 y,代表一个坐标。我们需要对坐标进行排序,排序的规则是 x 轴小的在前,如果 x 轴相等,则 y 小的在前。 35 | 36 | ```C++ 37 | struct P { 38 | int x, y; 39 | }; 40 | 41 | P arr[1000]; 42 | ``` 43 | 44 | 大家都知道,要对这样的结构体排序,一种做法是我们可以额外实现一个`cmp`函数作为`sort`排序函数的传参。如果不使用三元表达式,那么`cmp`函数是这样的: 45 | 46 | ```C++ 47 | bool cmp(const P &a, const P &b) { 48 | if (a.x == b.x) { 49 | return a.y < b.y; 50 | }else { 51 | return a.x < b.x; 52 | } 53 | } 54 | ``` 55 | 56 | 使用三元表达式的话,整个逻辑只有一行: 57 | 58 | ```C++ 59 | bool cmp(const P& a, const P& b) { 60 | return a.x == b.x ? a.y < b.y : a.x < b.x; 61 | } 62 | ``` 63 | 64 | 当时在我还是萌新的时候,看到这种写法惊呆了,觉得实在是太简洁太优雅了。怎么样,你们学到了吗? 65 | -------------------------------------------------------------------------------- /C++/26-switch语句.md: -------------------------------------------------------------------------------- 1 | ## switch 2 | 3 | 在日常的开发当中,我们经常会遇到一种情况,我们用一个变量表示状态。比如关闭-激活-完成,当我们需要判断状态的时候,就需要罗列`if-else`语句。 4 | 5 | ```C++ 6 | if (status == 'closed') { 7 | // todo 8 | }else if (status == 'activated') { 9 | // todo 10 | }else if (status == 'done') { 11 | // todo 12 | } 13 | ``` 14 | 15 | 如果只有少数几个还好,当我们要枚举的状态多了之后,写`if-else`就会非常繁琐。所以 C++当中提供了`switch`语句来代替简单的`if-else`的罗列。 16 | 17 | ```C++ 18 | switch(expression) { 19 | case constant1: 20 | //todo 21 | case constant2: 22 | //todo 23 | case constant3: 24 | //todo 25 | default: 26 | //todo 27 | } 28 | ``` 29 | 30 | 要注意的是,`switch`语句当中的`expression`只能是一个整数或者是枚举类型,不能是其他类型。比如像是`string`就不可以作为`switch`语句的 case,这个非常坑,很容易不小心写错。 31 | 32 | 所以上面的`if-else`语句可以改写成: 33 | 34 | ```C++ 35 | switch (status) { 36 | case 1: 37 | // todo1 38 | break; 39 | case 2: 40 | // todo2 41 | break; 42 | case 3: 43 | // todo3 44 | break; 45 | default: 46 | //todo 47 | } 48 | ``` 49 | 50 | 最后的`default`表示默认情况,也就是当之前的所有可能都不满足时会执行`defalut`标签下的内容。还有一点需要注意,`switch`语句有点像是路牌,它只是根据`expression`的值将代码跳转到对应的位置,并不是只运行对应标签的代码。 51 | 52 | 比如当我们的`status`为 1 时,代码会跳转到`todo1`处,在执行完`todo1`之后依然会继续往下执行`todo2`、`todo3`的代码。如果我们只希望执行`todo1`的代码,需要在末尾加上`break`,表示执行结束,跳出。这也是一个坑点,加不加`break`完全是两种效果。 53 | 54 | 数字 1、2、3 表示状态显然会导致含义不够明显,所以我们也可以使用枚举类型: 55 | 56 | ```C++ 57 | enum status {closed, done, activated}; 58 | 59 | status st; 60 | 61 | switch (st) { 62 | case closed: 63 | //todo 64 | break; 65 | case done: 66 | //todo 67 | break; 68 | case activated: 69 | //todo 70 | default: 71 | //todo 72 | } 73 | ``` 74 | -------------------------------------------------------------------------------- /C++/27-break和continue语句.md: -------------------------------------------------------------------------------- 1 | ## break 和 continue 2 | 3 | `break`和`continue`都是循环体当中经常使用的语句,并且也不只是 C++在其他语言当中同样存在。`break`和`continue`的存在,大大丰富了循环体的功能,这两者都是用来跳过部分代码,但是执行的细节有所不同,使用场景也有所区别。 4 | 5 | ### break 6 | 7 | `break`的含义是结束循环,当程序执行到`break`之后会直接跳出循环体,执行循环体之后的部分。可以被使用在任何循环当中(`for`循环,`while`循环和`do while`循环)。 8 | 9 | 比如: 10 | 11 | ```C++ 12 | int a[5] = {1, 6, 3, 10, 8}; 13 | 14 | for (int i = 0; i < 5; ++i) { 15 | if (a[i] >= 10) { 16 | break; 17 | } 18 | cout << a[i] << " "; 19 | } 20 | ``` 21 | 22 | 屏幕上输出的结果会是`1 6 3`,因为当遇到`a[i] = 10`的情况之后执行了`break`,直接跳出了`for`循环。循环体当中`break`语句之后的部分也不会执行。 23 | 24 | 另外,`break`只能跳出一重循环,如果我们使用了多重循环的嵌套,执行了`break`只能跳出当前循环,而不会跳出整个循环体。 25 | 26 | ```C++ 27 | for (int i = 0; i < n; ++i) { 28 | for (int j = 0; j < m; ++j) { 29 | if (condition) { 30 | break; 31 | } 32 | } 33 | // todo 34 | } 35 | ``` 36 | 37 | 比如在上面的示例当中,我们在`j`循环当中执行了`break`,会立即跳出`j`循环,但是外层的`i`循环依旧会继续执行。 38 | 39 | 那如果我们想要跳出多重循环应该怎么办呢? 40 | 41 | 大概有两种办法,一种是加入退出标记,手动多次执行`break`。 42 | 43 | ```C++ 44 | bool flag = false; 45 | for (int i = 0; i < n; ++i) { 46 | for (int j = 0; j < m; ++j) { 47 | if (condition) { 48 | flag = true; 49 | break; 50 | } 51 | } 52 | if (flag) break; 53 | } 54 | ``` 55 | 56 | 我们创建了一个`bool`型的变量,用来表示是否要跳出全部循环。之后我们在外层的循环当中加上对这个变量的判断,如果`flag`为`true`再执行一次手动退出。 57 | 58 | 当然我们也可以把它和循环体当中的判断条件合并,写成这样: 59 | 60 | ```C++ 61 | bool flag = false; 62 | for (int i = 0; i < n && !flag; ++i) { 63 | for (int j = 0; j < m; ++j) { 64 | if (condition) { 65 | flag = true; 66 | break; 67 | } 68 | } 69 | } 70 | ``` 71 | 72 | 除此之外还有其他几种变体,都大同小异就不一一列举了。 73 | 74 | 还有一种方法是使用`goto`语句: 75 | 76 | ```C++ 77 | for (int i = 0; i < n; ++i) { 78 | for (int j = 0; j < m; ++j) { 79 | if (condition) { 80 | goto outloop; 81 | } 82 | } 83 | } 84 | 85 | outloop: cout << "end" << endl; 86 | ``` 87 | 88 | 但众所周知`goto`语句非常危险,可能会导致很多意想不到的问题,因此非万不得已,不要使用`goto`语句。这里仅供参考。 89 | 90 | ### continue 91 | 92 | `continue`语句执行也会跳过语句之后的代码,但并不会退出循环,而是进入下一次循环当中。 93 | 94 | ```C++ 95 | int a[5] = {1, 6, 3, 10, 8}; 96 | 97 | for (int i = 0; i < 5; ++i) { 98 | if (a[i] >= 10) { 99 | continue; 100 | } 101 | cout << a[i] << " "; 102 | } 103 | ``` 104 | 105 | 上面这段代码的执行结果是`1 6 3 8`,中间的`a[i] == 10`的情况执行了`continue`跳过了循环体之后的所有逻辑,进入了下一重循环当中。 106 | 107 | 由于我们的代码逻辑非常简单,同样的逻辑其实也可以不用使用`continue`语句: 108 | 109 | ```C++ 110 | int a[5] = {1, 6, 3, 10, 8}; 111 | 112 | for (int i = 0; i < 5; ++i) { 113 | if (a[i] < 10) { 114 | cout << a[i] << " "; 115 | } 116 | } 117 | ``` 118 | 119 | 但如果循环体当中的代码非常复杂,相比于使用一个巨大的`if`语句,使用`continue`可以提高代码的可读性。这个需要我们根据实际情况具体问题具体分析,并没有标准答案。 120 | -------------------------------------------------------------------------------- /C++/28-指针和const.md: -------------------------------------------------------------------------------- 1 | ## 指针和 const 2 | 3 | 我们知道`const`关键字修饰的是不可变量,将它和指针一起使用,会有很多微妙的地方。 4 | 5 | 关于使用`const`来修饰指针,有两种不同的方式。第一种是让指针指向一个常量对象,这样可以防止使用该指针进行修改指向的值。第二种则是将指针本身声明为常量,可以防止改变指针指向的位置。下面我们来看下细节。 6 | 7 | ### 指向常量的指针 8 | 9 | 首先是指向常量的指针,含义是指针的类型是一个常量类型。所以写成: 10 | 11 | ```C++ 12 | const int * p; 13 | ``` 14 | 15 | 可以理解成`p`是一个指针,它的类型是`const int`,也就是常量类型。它既可以用来指向一个常量类型,也可以指向一个非常量类型,下方的这两种方式都是合法的: 16 | 17 | ```C++ 18 | int age = 23; 19 | const int* p = &age; 20 | 21 | const double price = 233; 22 | const double* pt = &price; 23 | ``` 24 | 25 | 但是反过来,将一个`const`类型的变量赋值给一个非`const`的指针是非法的: 26 | 27 | ```C++ 28 | const int age = 23; 29 | int* p = &age; // 非法 30 | ``` 31 | 32 | 如果非要这样做,可以使用`const_cast`运算符进行强制转换,这个我们会在之后的文章当中讨论。 33 | 34 | 另外还有一个很有意思的点,如果我们将一个非`const`类型的变量赋给了`const`类型的指针,虽然我们无法通过指针修改对应的值,但是通过变量修改却是可以的: 35 | 36 | ```C++ 37 | int age = 23; 38 | const int* p = &age; 39 | 40 | *p = 233; // 非法 41 | age = 233; // 合法 42 | ``` 43 | 44 | 还有,我们无法修改指针指向的值,但是修改指针指向的位置是可以的: 45 | 46 | ```C++ 47 | int age = 23; 48 | int price = 233; 49 | const int* p = &age; 50 | p = &price; 51 | ``` 52 | 53 | ### const 指针 54 | 55 | 上面我们介绍了指向`const`的指针,还有另外一种指针叫做`const`指针。`const`指针指的是指针本身是`const`修饰的,我们无法修改指针指向的位置。 56 | 57 | ```C++ 58 | int age = 23; 59 | int* const p = &age; 60 | ``` 61 | 62 | 但是我们修改指针指向的值是可以的: 63 | 64 | ```C++ 65 | *p = 2333; // 合法 66 | ``` 67 | 68 | ### 指针和内容都不可变 69 | 70 | 当然我们也可以将两种`const`叠加使用,让指针指向的对象以及对象的值都不可修改: 71 | 72 | ```C++ 73 | const int * const p = &age; 74 | ``` 75 | -------------------------------------------------------------------------------- /C++/29-函数指针.md: -------------------------------------------------------------------------------- 1 | ## 函数指针 2 | 3 | 函数指针顾名思义,就是指向函数的指针。 4 | 5 | 和数据类似,C++当中函数也有地址,函数的地址是存储函数机器语言代码的内存地址。我们可以将另外一个函数的地址作为参数传入函数,从而实现函数的灵活调用。 6 | 7 | ### 获取函数地址 8 | 9 | 获取函数地址的方法非常简单,只要使用函数名(后面不跟参数和括号)即可。比如我们有一个函数叫做`think`,那么`think()`是调用函数拿到结果,而`think`则是获取函数的地址。 10 | 11 | 如果我们想要将`think`函数当做参数传入另外一个函数,我们可以这么写: 12 | 13 | ```C++ 14 | func(think); 15 | ``` 16 | 17 | ### 声明函数指针 18 | 19 | 声明函数指针和声明函数类似,我们声明一个函数可以这么写: 20 | 21 | ```C++ 22 | double process(int); 23 | ``` 24 | 25 | 而我们声明函数指针则可以写成这样: 26 | 27 | ```C++ 28 | double (*pt)(int); 29 | ``` 30 | 31 | 如果我们把`(*pt)`替换成函数名的话,这其实就是一个函数的声明。如果`(*pt)`是函数的话,那么`pt`自然就是指向函数的指针了。 32 | 33 | ### 函数指针传参 34 | 35 | 如果我们要实现一个函数,它的一个参数是一个函数指针,它的写法和刚才一样: 36 | 37 | ```C++ 38 | double func(double x, double (*pt)(int)); 39 | ``` 40 | 41 | 在这个声明当中,它的第二个参数是一个函数指针。指向的函数接收一个`int`参数,返回一个`double`结果。 42 | 43 | ### 调用函数 44 | 45 | 最后, 我们来看下通过指针调用函数的部分。 46 | 47 | 其实也非常简单,因为我们前面说了`(*pt)`的效果和函数是一样的,我们之前通过函数名调用函数,那么我们只需要改成通过`(*pt)`调用即可。 48 | 49 | 如: 50 | 51 | ```C++ 52 | double process(int); 53 | double (*pt)(int); 54 | 55 | pt = process; 56 | cout << (*pt)(5) << endl; 57 | ``` 58 | -------------------------------------------------------------------------------- /C++/3-谷歌编码规范.md: -------------------------------------------------------------------------------- 1 | ### 变量规范 2 | 3 | C++当中变量的声明由变量类型 + 变量名组成。 4 | 5 | 关于 C++的命名有几种规则: 6 | 7 | - 名称中只能使用字母、数字和下划线 8 | - 名称的第一个字符不能是数字 9 | - 大小写敏感 10 | - 不能使用 C++关键字(如 class、void 等) 11 | - 用户自定义的标识符中不能连续出现两个下划线,也不能以下划线紧跟大写字母开头,此外定义在函数体外的标识符不能以下划线开头 12 | - C++对于名称长度没有限制,但部分平台有限制 13 | 14 | 对于初学者来说,由于编写的代码以及阅读的代码总量不够,对于什么是合理、完善的编码规范往往是比较困惑的。对于这点,我们可以参考谷歌的 C++编码规范。 15 | 16 | ### 总则 17 | 18 | 总的原则是尽可能使用描述性的命名,不吝啬变量长度,因为相比之下让代码容易理解比容易敲出来更重要。 19 | 20 | 我们分别来看几个正面例子和反面例子,首先是正面例子: 21 | 22 | ```C++ 23 | int price_count_reader; // 无缩写 24 | int num_errors; // "num" 是一个常见的写法 25 | int num_dns_connections; // 人人都知道 "DNS" 是什么 26 | ``` 27 | 28 | 这三个变量名的优点也已经写在注释里了,无让人歧义的缩写,变量名含义充分。再来看几个反面例子: 29 | 30 | ```C++ 31 | int n; // 毫无意义. 32 | int nerr; // 含糊不清的缩写. 33 | int n_comp_conns; // 含糊不清的缩写. 34 | int wgc_connections; // 只有贵团队知道是什么意思. 35 | int pc_reader; // "pc" 有太多可能的解释了. 36 | int cstmr_id; // 删减了若干字母. 37 | ``` 38 | 39 | 主要原因是掺入了太多的缩写,导致可读性几乎为零,不仅别人读不懂,就连写出这个代码的作者很快也会忘记它原本的含义。 40 | 41 | ### 文件命名 42 | 43 | 文件名全部要小写,可以包含下划线以及字符`-`,最好使用下划线。 44 | 45 | 比如:`my_class.cc`,`myclass.cc`等,这个只是谷歌的标准,使用.cpp 后缀也是可以的,只要统一就行。 46 | 47 | ### 类型命名 48 | 49 | 类型命名采用大驼峰命名,不包含下划线,如:`MyExcitingClass`,`HelloWorldClass`等。 50 | 51 | 所谓类型命名包括类,结构体,类型定义(`typedef`),枚举,类型模板参数,它们均使用相同的约定。即大写字母开头,每个单词的开头也为大写,即大驼峰。 52 | 53 | 如: 54 | 55 | ```C++ 56 | // 类和结构体 57 | class UrlTable { ... 58 | class UrlTableTester { ... 59 | struct UrlTableProperties { ... 60 | 61 | // 类型定义 62 | typedef hash_map PropertiesMap; 63 | 64 | // using 别名 65 | using PropertiesMap = hash_map; 66 | 67 | // 枚举 68 | enum UrlTableErrors { ... 69 | ``` 70 | 71 | ### 变量命名 72 | 73 | C++的变量没有像 Java 一样遵循小驼峰,而是一律小写,单词和单词之间以下划线连接。 74 | 75 | 如: 76 | 77 | ```C++ 78 | string table_name; // 好 - 用下划线. 79 | string tablename; // 好 - 全小写. 80 | 81 | string tableName; // 差 - 混合大小写 82 | ``` 83 | 84 | 但如果是类中的数据成员(成员变量),则变量的末尾需要额外的下划线: 85 | 86 | ```C++ 87 | class TableInfo { 88 | ... 89 | private: 90 | string table_name_; // 好 - 后加下划线. 91 | string tablename_; // 好. 92 | static Pool* pool_; // 好. 93 | }; 94 | ``` 95 | 96 | 但结构体除外,仍然和普通变量一样命名。 97 | 98 | ```C++ 99 | struct UrlTableProperties { 100 | string name; 101 | int num_entries; 102 | static Pool* pool; 103 | }; 104 | ``` 105 | 106 | C++的变量命名方法类似 Python 区别于 Java,对于学过 Java 的同学来说可能有些别扭。 107 | 108 | ### 常量命名 109 | 110 | 声明为`constexpr`和`const`的变量,或者是其他没有显示定义但是在程序运行当中保持不变的,命名是以 k 开头,并且大小写混合,如: 111 | 112 | ```C++ 113 | const int kDaysInAWeek = 7; 114 | ``` 115 | 116 | ### 函数命名 117 | 118 | 函数命名同样遵循大驼峰命名法,即首字母以及每个单词首字母大写。 119 | 120 | 对于特殊的缩写单词,通常将它们视作是普通单词,而不会全部大写,如: 121 | 122 | ```C++ 123 | void GetUrl(); // 正确 124 | void GetURL(); // 错误 125 | ``` 126 | 127 | ### 命名空间 128 | 129 | 全部小写命名 130 | 131 | ### 宏命名 132 | 133 | 宏命名为全部大写,并且以下划线分割,如: 134 | 135 | ```C++ 136 | #define ROUND(x) .. 137 | ``` 138 | 139 | 不过不推荐在代码中使用宏。 140 | 141 | ### 枚举命名 142 | 143 | 与常量或宏一致,即`kEnumName`或`ENUM_NAME`。个人更倾向于后者。 144 | 145 | > 参考资料 谷歌编码规范及相关博客 146 | -------------------------------------------------------------------------------- /C++/30-函数指针进阶.md: -------------------------------------------------------------------------------- 1 | ## 函数指针进阶 2 | 3 | 简单的函数指针比较简单,但对于复杂的情况则显得有些恐怖。下面我们来看下 C++ primer 当中提供的一些例子: 4 | 5 | ```C++ 6 | const double* f1(const double ar[], int n); 7 | const double* f2(const double [], int); 8 | const double* f3(const double *, int); 9 | ``` 10 | 11 | 这三个函数看起来长得不一样,但其实是等价的。因为在函数参数列表当中,数组和指针是等价的。其次我们可以在函数的原型中省略掉变量名,因此`const double ar[]`可以简化成`const double []`,也可以写成`const double *`。 12 | 13 | 有了这三个函数之后,假设我们要声明一个指针,指向这三个函数。根据我们前文当中说过的,可以将函数名替换成`(*pt)`来实现: 14 | 15 | ```C++ 16 | const double* (*pt)(const double *, int) = f1; 17 | ``` 18 | 19 | 其实这个语句看起来就有些复杂了,整个语句的可读性很差。如果不是知道这里用的是一个函数指针,乍一看想要看明白估计不太容易。我们可以分成两个部分来理解,其中`const double *`是一个整体,表示函数的返回值类型是一个`const double *`也就是一个常量浮点数的地址。其次`(*pt)`是一个整体,代替了函数名,表示这是一个指向函数的指针。 20 | 21 | 在 C++11 当中提供了叫做`auto`的新特性,它可以帮助变量自动识别对应的类型,可以解决一些类型特别复杂的问题,比如: 22 | 23 | ```C++ 24 | auto p2 = f2; 25 | ``` 26 | 27 | 函数指针有两种调用方法,除了可以使用`(*p2)`的方式调用之外,也可以直接使用名称调用: 28 | 29 | ```C++ 30 | const double* x = p2(ar, 3); 31 | const double* y = (*p2)(ar, 3); 32 | ``` 33 | 34 | 显然前者更好,更清楚。这里其实有一个疑问,为什么这两种方式都可以执行呢?这是因为当我们执行`auto p2 = f2`的时候,其实是执行的`auto p2 = &f2`,C++会隐式地将函数转换成函数的地址。因为函数的值本身就是一个地址,所以这两种方式才都能正确地运行。 35 | 36 | 问题还没有结束,假如我们要定义一个指向函数的指针数组呢?这应该怎么声明? 37 | 38 | 也就是`const double* (*pt)(const double *, int)`这样一个类型的数组,它应该怎么声明,这个方括号应该放在那里? 39 | 40 | 正确答案是放在括号里: 41 | 42 | ```C++ 43 | const double* (*pt[3])(const double *, int); 44 | ``` 45 | 46 | 因为运算符`[]`的优先级高于`*`,因此`*pt[3]`表示`pt`是一个长度为 3 的指针数组。其他的内容表明了该指针的类型。 47 | 48 | 由于我们定义的是一个数组,所以这里不能使用`auto`,因为自动类型推断只能用于单值初始化而不能用于初始化列表。 49 | 50 | 到这里还没结束,还有更恐怖的,如果我们想要定义一个指向这个数组的指针,应该怎么办呢?如果使用`auto`可以写成: 51 | 52 | ```C++ 53 | auto ptr = &pt; 54 | ``` 55 | 56 | 如果不使用`auto`呢?首先我们可以想到,这个声明是基于`pt`的,我们需要在`pt`的声明上加上一个`*`,但问题是加在哪里呢? 57 | 58 | 进一步分析,会发现我们需要指出这是一个指针,而不是数组。意味着核心的部分应该写成`(*ptr)[3]`,表示这是一个指向长度为 3 的数组的指针。因为`[]`的优先级更高,所以需要使用括号。如果写成`*ptr[3]`表示这是长度为 3 的指针数组。 59 | 60 | 我们进一步倒推,`(*ptr)[3]`这个数组当中的元素是什么类型呢?是指向函数的指针,所以写出来结果是这样: 61 | 62 | ```C++ 63 | const double *(*(*ptr)[3])(const double*, int) = &pt; 64 | ``` 65 | 66 | 很明显,这样的定义非常非常的难以理解。而且这还不是最复杂的情况,比如函数的返回类型又是一个指向一个函数的指针……明摆着告诉我们含义我们仍然要推敲一会,如果在一段不明的代码当中遇到,可能会直接抓狂吧…… 67 | 68 | 也正因此,C++11 当中推出了`auto`特性,可以简化这种情况。 69 | 70 | 多说一句题外话,golang 语言当中将变量的类型放在变量的后面而不是前面,其中一个原因就是为了解决类似情况的复杂性。 71 | 72 | 如果是 golang 来定义同样的内容,会是这样的: 73 | 74 | ```golang 75 | func f2(arr []float64, n int) *float64 { 76 | // todo 77 | } 78 | 79 | // 函数指针 80 | var p1 func([]float64, int) *float64 = f2; 81 | // 函数指针数组 82 | var pt [3]func([]float64, int) *float64; 83 | // 函数指针数组的指针 84 | var ptr *[3]func([]float64, int) *float64 = &pt; 85 | ``` 86 | 87 | 很明显,虽然变量类型写在变量后面刚开始会不太习惯,但是很明显这样要清晰很多。 88 | -------------------------------------------------------------------------------- /C++/31-内联函数.md: -------------------------------------------------------------------------------- 1 | ## 内联函数 2 | 3 | 内联函数是 C++当中为了提高程序运行效率的设计,老实讲我没有在其他语言当中看到类似的设计。它和常规函数之间的主要区别不在于编写的方式,而是在于 C++编译器会将内联函数组合到程序当中执行。 4 | 5 | 要解释这个过程会稍稍有些复杂,我们需要从编译的过程说起。对于编译型语言而言,编译器做的事情是把人类写出来人能读懂的代码翻译成机器能够识别、执行的机器语言,一般是一串十六进制的指令。随后计算机逐步执行这些指令,完成我们想要的功能。 6 | 7 | 当我们调用函数时,其实本质上是指令跳转,先记录下当前运行的指令位置,跳转到函数所在的指令位置进行执行,执行完成之后再跳转回来。这个当中除了跳转之外,还会发生一些参数的传递和拷贝,需要一定的开销。 8 | 9 | 而使用内联函数,本质上可以理解成使用相应的函数代码代替了函数调用。可以简单理解成把函数当中的代码拷贝了一份粘贴到了函数调用的位置,代替了函数跳转。举个例子,比如说我们有一个函数来计算坐标到原点的距离: 10 | 11 | ```C++ 12 | include 13 | 14 | double distance(double x, double y) { 15 | return sqrt(x * x + y * y); 16 | } 17 | 18 | double x = 3.0, y = 4.0; 19 | double d = distance(x, y); 20 | ``` 21 | 22 | 当我们使用了内联函数之后,它相当于把函数的代码拷贝了一份粘贴到了调用的位置: 23 | 24 | ```C++ 25 | double x = 3.0, y = 4.0; 26 | double d = sqrt(x * x + y * y); 27 | ``` 28 | 29 | 这也就是内联的含义,使用了内联函数之后,程序无须跳转到另外一个位置进行执行,可以节省掉跳转所带来的开销。因此运行效率要比普通函数更快,但代价是需要占用更多的内存。比如我们调用了 10 次内联函数,相当于代码拷贝了十份。 30 | 31 | 内联函数的使用非常简单,就是在函数定义之前加上`inline`关键字。 32 | 33 | 需要注意的是,有的时候我们虽然加上了`inline`关键字但编译器并不一定会遵照执行。有些编译器会有函数规模的限制,并且会限制内联函数禁止调用自己,也就是不能递归。 34 | 35 | 还有一点是内联函数虽然有内联机制,但是函数的传参依然是值传递,也就是说会发生拷贝,和普通函数一致。 36 | 37 | 在 C 语言当中没有`inline`特性,C 语言是使用宏定义来实现类似的功能。但宏定义并不是通过参数传递,而是代替机械替换实现的。 38 | 39 | 比如: 40 | 41 | ```C++ 42 | #define SQUARE(x) x*x 43 | 44 | double a = SQUARE(3.4 + 3.5); 45 | ``` 46 | 47 | 这样我们得到的结果会是`3.4 + 3.5 * 3.4 + 3.5`,也就是说宏定义只是机械地替换代码,并不是函数式的调用。所以要实现类似`inline`函数的效果,可以使用括号: 48 | 49 | ```C++ 50 | #define SQUARE(x) ((x) * (x)) 51 | ``` 52 | -------------------------------------------------------------------------------- /C++/32-引用.md: -------------------------------------------------------------------------------- 1 | ## 引用 2 | 3 | 引用是 C++新增的特征,C 语言当中没有。 4 | 5 | 引用是给已经定义的变量一个别名,可以简单理解成同一个变量的昵称。既然是昵称或者是别名,显然它和原本的变量名有着同样的效力。所以我们对别名进行修改,原本的变量值也一样会发生变化。 6 | 7 | 我们通过符号&来表明引用,比如下面这个例子,我们创建了 a 变量的一个引用 b。 8 | 9 | ```C++ 10 | int a = 3; 11 | int &b = a; 12 | b++; 13 | cout << a << endl; 14 | ``` 15 | 16 | 由于 b 是 a 的一个引用,本质上来说它们是同一个变量,只不过名称不同。所以我们对 b 修改,等价于对 a 进行同样的修改。所以输出的结果是 4。 17 | 18 | 也就是说我们需要把引用变量和原变量当成是同样的变量,只不过名称不同,其中一个发生变化,另外一个一样会生效。 19 | 20 | 看上去有些像是指针,因为创建指针也能有类似的效果: 21 | 22 | ```C++ 23 | int a = 3; 24 | int *p = &a; 25 | 26 | *p++; 27 | cout << a << endl; 28 | ``` 29 | 30 | 但是引用和指针还是有些区别,这个问题在 C++相关的面试当中经常会问到,也是作为基本功的考察之一。 31 | 32 | 首先一个区别是,引用必须在声明的时候就进行初始化,没办法先声明再赋值: 33 | 34 | ```C++ 35 | int *pt; // 合法 36 | int &b; // 非法 37 | ``` 38 | 39 | 从这个角度来说,引用更接近`const`指针,一旦与某个变量关联就不能再指向其他变量: 40 | 41 | ```C++ 42 | int &b = a; 43 | // 等价于 44 | int *const pt = &a; 45 | ``` 46 | 47 | 在这个例子当中,`b`等价于`*pt`。 48 | 49 | 如果我们输出引用和原变量的地址,会得到同样的结果: 50 | 51 | ```C++ 52 | int a = 3; 53 | int &b = a; 54 | 55 | cout << &a << " " << &b << endl; 56 | ``` 57 | 58 | ### 函数引用传递 59 | 60 | 其实到这里有一个问题,既然引用只是别名,我们已经有了原本的变量名可以用了,又何必多此一举创建变量的引用呢? 61 | 62 | 所以引用不是为了顺序执行的逻辑创建的,一个最常见的使用场景就是函数参数传递的时候,可以设置函数接收的变量类型为引用。如: 63 | 64 | ```C++ 65 | void swap1(int& a, int& b) { 66 | int temp = b; 67 | b = a; 68 | a = temp; 69 | } 70 | 71 | void swap2(int a, int b) { 72 | int temp = b; 73 | b = a; 74 | a = temp; 75 | } 76 | ``` 77 | 78 | 我们创建了两个`swap`函数,其中一个传递的参数是引用,另外一个就是普通的值传递。如果大家去分别调用这两个函数进行尝试,会发现`swap2`函数没有生效。 79 | 80 | 因为值传递的时候,会发生拷贝,也就是说函数内部接受的其实是变量的拷贝。我们对于拷贝无论如何修改也不会影响原值,而传引用就不一样了。前面说过,引用和原变量是等价的。我们对引用进行修改等价于对原变量进行修改。 81 | 82 | 这样的话,我们就可以实现在函数体内部对外部传入的参数进行修改。在一些特殊的场景当中,非常方便。比如一些复杂的树形数据结构,通过使用引用可以大大降低代码的编写难度。 83 | 84 | 除此之外,使用引用还有一个好处,既然我们传递的引用和原值是等价的。那么也就免去了拷贝变量的开销,如果我们传递的是`int`,`double`这样的变量还好,如果是一个包含大量元素的容器,如`vector`,`set`,`map`等,使用引用传递可以带来明显的效率提升,也会降低内存开销。 85 | -------------------------------------------------------------------------------- /C++/33-引用与const.md: -------------------------------------------------------------------------------- 1 | ### 引用与 const 2 | 3 | 前文当中说过,我们可以让函数接收一个引用变量,从而免去变量拷贝的开销,达到提升程序运行效率的目的。 4 | 5 | 如果我们想要传递引用,但又不希望在函数内部对引用的变量进行修改,以免影响外部变量。我们可以使用常量引用,也就是加上`const`修饰符。 6 | 7 | ```C++ 8 | double sqrt(const double &x); 9 | ``` 10 | 11 | 由于我们加上了`const`修饰符,当我们在函数内部对引用进行修改的时候,会触发编译器的报错。一般来说,如果传递的只是基本类型的变量,我们其实没有必要这么操作,直接值传递即可。这种做法一般用在传递一些大型结构体或者是大型容器的时候。 12 | 13 | 这里有一个小细节需要当心,由于我们传递的是引用,需要保证传递的参数是一个实参,而不是表达式。如这样的代码编译时会报错: 14 | 15 | ```C++ 16 | double distance(double &x, double &y) { 17 | return sqrt(x * x + y * y); 18 | } 19 | 20 | int main() { 21 | double x = 3.0, y = 4.0; 22 | cout << distance(x + 3.0, y + 4.0); 23 | } 24 | ``` 25 | 26 | 报错的原因在于,函数`distance`接收的是一个`double`类型的引用,而我们传递的却是`x+3`这样的表达式。显然表达式没有对应的引用。所以编译器会报错,告诉我们参数类型不匹配: 27 | 28 | ![](https://tva1.sinaimg.cn/large/008i3skNgy1gweff16ligj311y0d6gnj.jpg) 29 | 30 | 但神奇的是,如果我们把函数签名稍微改一下,加上`const`修饰符,会发现报错消失了: 31 | 32 | ```C++ 33 | double distance(const double &x, const double &y) { 34 | return sqrt(x * x + y * y); 35 | } 36 | ``` 37 | 38 | 这并不是编译器的 bug,而是编译器针对`const`引用做了特殊处理。当编译器发现传入的不是`double`类型的变量的时候,它会创建一个临时的无名变量,将这个临时变量初始化成`x+3.0`,然后再传入这个临时变量的引用。C++只会对`const`引用参数执行这个操作。 39 | 40 | 除了表达式之外,如果变量的类型不匹配也一样会创建临时变量。这些临时变量只会在函数调用期间存在,函数运行结束之后,编译器会将其删除。 41 | 42 | 为什么会有这样的设计呢?C++ Primer 当中提供了这样一个例子: 43 | 44 | ```C++ 45 | void swapr(int &a, int &b) { 46 | int temp = b; 47 | b = a; 48 | a = temp; 49 | } 50 | 51 | long a = 3, b = 5; 52 | swapr(a, b); 53 | ``` 54 | 55 | 在早期 C++没有严格限制的情况下,这段代码会发生什么呢? 56 | 57 | 由于类型不匹配,所以编译器会创建两个临时的`int`变量,但它们初始化成 3 和 5,再传入函数当中。然后执行函数当中交换变量的逻辑,但问题是,我们交换的是两个临时变量,原变量并不会生效。 58 | 59 | 所以后来版本的 C++优化了这个问题,禁止了传递引用时创建临时变量。而当引用有`const`修饰时并不会对原值进行修改,并不会影响逻辑和结果,所以豁免了这个禁令。 60 | 61 | ### const 修饰符的优点 62 | 63 | 在函数签名当中,如果要接收引用,我们要尽可能使用`const`,我们来看下这样做的好处: 64 | 65 | - 可以避免无意中修改数据 66 | - 可以处理`const`和非`const`参数,否则,只能接受非`const`变量 67 | - 可以接受临时变量 68 | -------------------------------------------------------------------------------- /C++/34-引用和指针的区别.md: -------------------------------------------------------------------------------- 1 | ## 引用与指针的区别 2 | 3 | 指针和引用的原理非常的相似,所以很多时候尤其是面试的时候经常会拿来比较。 4 | 5 | 本文来梳理一下引用和指针的一些异同。 6 | 7 | ### 相同点 8 | 9 | 两者都是关于地址的概念。 10 | 11 | 指针本身是一个变量,它存储的值是一块内存地址,而引用是某一个内存的别名。我们可以使用指针或引用修改对应内存的值。 12 | 13 | ### 区别 14 | 15 | - 引用必须在声明时初始化,而指针可以不用 16 | 17 | 我们无法声明一个变量引用再给它赋值,只能在声明的同时进行初始化: 18 | 19 | ```C++ 20 | int a = 3; 21 | int &b; // 非法 22 | int &c = a; // 合法 23 | ``` 24 | 25 | 而指针没有这个限制: 26 | 27 | ```C++ 28 | int *p; // 合法 29 | ``` 30 | 31 | - 引用只能在声明时初始化一次,之后不能指向其他值,而指针可以 32 | 33 | 引用一旦声明无法更改,但指针可以。某种程度上来说,引用类似于常量指针。 34 | 35 | ```C++ 36 | int a = 3; 37 | int &b = a; 38 | int const *p = &a; 39 | ``` 40 | 41 | - 引用必须指向有效变量,指针可以为空 42 | 43 | 这是两者一个使用上巨大的区别,我们拿到一个引用可以放心地使用, 因为它一定不会为空。而指针则不然,有可能为空指针,必须要经过判断才能使用。 44 | 45 | - `sizeof`运算结果不同 46 | 47 | `sizeof`函数可以计算变量内存块的大小,但如果我们对指针使用`sizeof`得到的是指针这个变量本身的占用内存大小,而不是指针指向的变量的内存大小。而引用则没有这个问题。 48 | 49 | - 有指针的引用,但是没有引用的指针 50 | 51 | 我们先来看引用的指针: 52 | 53 | ```C++ 54 | int a = 3; 55 | int &b = a; 56 | int *p = &b; 57 | ``` 58 | 59 | 这段代码并不会报错,但如果我们真的去运行了,会发现`p`就是一个普通的`int`型指针,它指向的是变量`a`。因为`b`是一个引用,它的地址和`a`相同。所以我们定义一个指向`b`的指针,实际上就是定义指向`a`的指针。这也是指向引用的指针不存在的原因。 60 | 61 | 再来看看指针的引用,指针的引用是存在的,也很好理解,本质上就是指针的一个别名: 62 | 63 | ```C++ 64 | int a = 3; 65 | int *p = &a; 66 | int *&pt = p; 67 | ``` 68 | 69 | `pt`也可以指向别的变量,也可以修改解引用的值,使用上它和`p`没有任何区别。 70 | 71 | 除了上面说的这些之外,指针和引用还在一些细小的方面有一些差别。例如自增和自减的含义不同,指针的自增和自减代表的是指针的移动,而引用的自增自减则是变量的值发生变化。 72 | -------------------------------------------------------------------------------- /C++/35-引用结构体.md: -------------------------------------------------------------------------------- 1 | ## 引用与结构体 2 | 3 | 最后, 来聊聊将引用和结构体。 4 | 5 | 结构体是我们自定义的复合类型,本质上也是一种变量类型,所以一样可以使用引用。传递结构体引用的方式和其他变量一样: 6 | 7 | ```C++ 8 | struct P { 9 | int x, y; 10 | }; 11 | 12 | void set_axis(P& a, P& b); 13 | ``` 14 | 15 | 前文当中也曾说过,虽然引用在基本类型上一样适用,但一般在实际使用当中,不在基本变量类型上使用引用。倒不是有什么问题,而是没有必要,毕竟基本变量类型占据的内存太小了,值传递和引用传递带来的差别几乎可以忽略不计。 16 | 17 | 因此使用得比较多的就是引用传递结构体,因为结构体当中的成员变量往往比较复杂,通过引用传递可以避免结构体的整体拷贝,可以节省时间和内存。 18 | 19 | 不仅如此,我们还可以通过函数返回引用: 20 | 21 | ```C++ 22 | P& return_ref(P& a); 23 | ``` 24 | 25 | 返回引用的目的和传递引用参数的目的是一样的,为了节省时间和内存。 26 | 27 | 如果函数返回的不是引用,而是结构体的值的话,调用代码可能是这样的: 28 | 29 | ```C++ 30 | P m = return_ref(a); 31 | ``` 32 | 33 | `return_ref`这个函数的返回结果会先赋值到一个临时的位置,然后再复制给`m`。这和我们传递结构体参数的开销是一样的,如果我们返回的类型是引用,那么则可以节省掉这个开销。 34 | 35 | 但是,这里有一个坑。 36 | 37 | 我们通过函数返回的引用,不能是函数终止时就不存在的内存单元,也就是不能是临时变量。比如下面这个例子就是不行的: 38 | 39 | ```C++ 40 | P& return_ref(P& a) { 41 | P cur = a; 42 | return cur; 43 | } 44 | ``` 45 | 46 | 我们在函数当中将传入的结构体`a`拷贝了一份,对这个拷贝体进行了返回。这样的代码从逻辑上看是没有问题的,但问题是我们创建的`cur`是一个临时变量,当函数返回的时候就会被销毁,不再存在,于是就会导致一些未知的错误。 47 | 48 | 所以如果要使用函数返回引用的话,一定要返回外部传入的引用或者全局变量的引用,而不能在函数内部临时创建。 49 | 50 | 除此之外,返回引用还有另外一个坑点,我们来看代码: 51 | 52 | ```C++ 53 | P a,b; 54 | 55 | return_ref(a) = b; 56 | ``` 57 | 58 | 这样的语法是被允许的,因为`return_ref`函数返回的是一个引用,我们当然可以对一个引用的值进行修改。有的情况下这一样会产生问题,如果你不想要这样的情况被允许,也有办法,我们可以使用`const`关键字,将返回的结果限制成不可修改: 59 | 60 | ```C++ 61 | const P& return_ref(P& a); 62 | ``` 63 | -------------------------------------------------------------------------------- /C++/36-默认参数.md: -------------------------------------------------------------------------------- 1 | ## 默认参数 2 | 3 | C++当中的支持默认参数,如果你学过 Python,那么想必对此不会陌生。C++中的默认参数的用法和 Python 基本一致。 4 | 5 | 使用默认参数的方法非常简单,也就是我们在函数声明的时候,就为某些参数指定好默认值。当我们调用函数的时候,如果没有传入对应的参数,那么则使用默认值。 6 | 7 | 比如: 8 | 9 | ```C++ 10 | void func(int a, int b=2, int c=3, int d=4) { 11 | cout << a << " " << b << " " << c << " " << d << endl; 12 | } 13 | ``` 14 | 15 | 在这个函数 func 当中,我们定义了三个变量的默认值,那么我们在调用的时候,以下这几种方式都是可以的: 16 | 17 | ```C++ 18 | func(1); // 1 2 3 4 19 | func(1, 3); // 1 3 3 4 20 | func(1, 3, 5); // 1 3 5 4 21 | func(1, 3, 4, 8); // 1 3 4 8 22 | ``` 23 | 24 | 另外,默认参数的值也不一定是定值,也可以是一个表达式,例如: 25 | 26 | ```C++ 27 | class Test { 28 | public: 29 | static int getValue() { 30 | return 1; 31 | } 32 | 33 | int func(int a, int b = getValue()) { 34 | return b; 35 | } 36 | } ; 37 | ``` 38 | 39 | 和 Python 一样,C++当中也有对默认参数的限制:如果某个参数是默认参数,那么从它开始之后的所有参数必须都是默认参数。 40 | 41 | ```C++ 42 | void func(int a, int b=3, int c=4); // 合法 43 | void func(int a=3, int b=4, int c=5); // 合法 44 | void func(int a, int b=3, int c); // 非法 45 | ``` 46 | 47 | 默认参数是一个非常好用的特性,熟练使用可以大大降低编码的复杂度,可以实现各种高级操作。 48 | 49 | 但默认参数也有一个大坑,需要注意,就是和函数重载一起使用的时候。比如我们有这样两个函数: 50 | 51 | ```C++ 52 | void func(int a); 53 | void func(int a, int b=3); 54 | ``` 55 | 56 | 这两个函数虽然函数名一样,但是接收的参数不同。因此会被视作是函数重载,编译器会根据我们传入的参数进行判断究竟调用哪一个。但如果我们这样调用函数,则会引起歧义: 57 | 58 | ```C++ 59 | func(3); 60 | ``` 61 | 62 | 因为编译器会不知道究竟你是在调用哪一个函数,于是就会引发下列的报错: 63 | 64 | ![](https://tva1.sinaimg.cn/large/008i3skNgy1gwhw1wznwzj30sy0a4wf2.jpg) 65 | -------------------------------------------------------------------------------- /C++/37-函数重载.md: -------------------------------------------------------------------------------- 1 | ## 函数重载 2 | 3 | 函数重载还有一个别名叫函数多态,其实我个人感觉函数多态这个名字更好理解更恰当一些。 4 | 5 | 函数多态是 C++在 C 语言基础上的新特性,它可以让我们使用多个同名函数。当然这些同名函数的参数是要有区别的,我们在函数调用的时候,编译器会自动根据我们传入的参数,从多个同名函数当中找到我们调用的那一个。和面向对象里的多态的概念很接近。 6 | 7 | 我们在定义函数的时候,编译器只会查看参数的数目和类型,而不会理会参数的名称。只要参数的数量以及类型不完全相同,就会被认为是不同的函数。比如: 8 | 9 | ```C++ 10 | void print(const char *str, int width); 11 | void print(double d, int width); 12 | void print(long l, int width); 13 | void print(int i, int width); 14 | void print(const char *str); 15 | ``` 16 | 17 | 上面列举的 5 个函数它们彼此之间的函数参数的数量和类型都不完全相同,因此会被视为是不同的函数。我们在使用的时候编译器会根据我们传入的参数使用对应的函数。 18 | 19 | ```C++ 20 | print('pancakes', 15); // use 1 21 | print('pancakes'); // use 5 22 | print(1999.0, 10); // use 2 23 | print(199, 23); // use 4 24 | print(199L, 15); // use 3 25 | ``` 26 | 27 | 这当然没有问题,如果我们这样使用呢: 28 | 29 | ```C++ 30 | unsigned year = 2021; 31 | print(year, 6); 32 | ``` 33 | 34 | 我们可以发现我们这里传入的参数类型是`unsigned int`,它不和任何函数的入参类型匹配。这个时候编译器并不会放弃,而是会尝试使用标准类型转换强制进行匹配。但问题来了,我们有三个版本的函数的第一个入参是数字类型,于是就有了三种变量转换的方式。这个时候 C++将拒绝这种函数调用,进行报错。 35 | 36 | 同样,一些看起来彼此不同的参数也是不能共存的,比如: 37 | 38 | ```C++ 39 | double cube(double x); 40 | double cube(double &x); 41 | ``` 42 | 43 | 看起来一个是值传递一个是引用传递,但是对于编译器来说,显然它是无法分辨我们究竟要调用哪一个的。 44 | 45 | 还有一点需要注意,就是`const`修饰符。 46 | 47 | ```C++ 48 | void dribble(char *bits); //1 49 | void dribble(const char *bits); //2 50 | ``` 51 | 52 | `dribble`函数有两个类型,一个用于`const`指针,一个用于常规指针,编译器将会根据实参是否为`const`来决定使用哪个函数。因为将非`const`值赋给`const`变量是合法的,但反之是非法的。 53 | 54 | 另外,编译器区分函数是根据函数的参数数量和类型并不是根据函数的返回值。所以下面的两个声明是有问题的: 55 | 56 | ```C++ 57 | long gronk(int n, float m); 58 | double gronk(int n, float m); 59 | ``` 60 | 61 | 因为它们的参数数量以及类型都是一样的,尽管返回类型不同,但编译器依然无法区分。 62 | 63 | 这个问题经常在面试当中出现,面试官会故意挖坑问你,函数重载的依据是什么。如果两个函数的返回类型不同,但是参数一样,能不能重载。很多同学对重载的概念记忆不是非常深刻,面试的时候脑子一热就中招了,所以一定要注意。 64 | -------------------------------------------------------------------------------- /C++/38-右值引用.md: -------------------------------------------------------------------------------- 1 | ## 右值引用 2 | 3 | ### 左值和右值 4 | 5 | 在我们之前的文章当中,介绍的都是左值引用。C++11 在左值引用的基础上推出了右值引用,由于是新特性,加上使用的频率也不是很高,有一定的学习成本。 6 | 7 | 我们先把引用这个概念抛开,先来看看什么是左值什么是右值。其实很简单,左值可以取地址,位于等于号的左侧。而右值没办法取地址,位于等于号的右侧。 8 | 9 | ```C++ 10 | int a = 4; 11 | ``` 12 | 13 | 比如我们定义了一个`int`型的变量 a,让它的值等于 4。其中 a 位于等于号的左侧,并且我们可以求 a 的地址。而 4 位于等于号的右侧,我们没有办法对 4 取地址。所以 a 是左值,4 是右值。 14 | 15 | 再比如: 16 | 17 | ```C++ 18 | int test() { 19 | return 4; 20 | } 21 | 22 | int a = test(); 23 | ``` 24 | 25 | 同样,a 位于等于号的左侧,有办法取地址是个左值。而`test()`是一个临时值没办法取地址,是个右值。 26 | 27 | 所以到这里就比较清楚了,有地址的变量就是左值,没有地址的常量值、临时变量就是右值。 28 | 29 | ### 左值引用和右值引用 30 | 31 | 明白了左值、右值的概念再来看看左值引用、右值引用就简单了。左值引用顾名思义就是能够指向左值不能指向右值的引用。 32 | 33 | ```C++ 34 | int a = 4; 35 | int &b = a; // 合法 36 | int &c = 4; // 非法 37 | ``` 38 | 39 | 但是左值引用也有例外,就是使用`const`修饰的左值引用是可以指向右值的: 40 | 41 | ```C++ 42 | const int &b = 4; 43 | ``` 44 | 45 | 因为`const`修饰的引用无法再更改,所以可以指向右值。如果大家度过 STL 代码的话,会发现其中一些函数的入参是`const &`目的就是为了能够兼容传参是常量的情况。比如`vector`当中的`push_back`: 46 | 47 | ```C++ 48 | void push_back (const value_type& val); 49 | ``` 50 | 51 | 右值引用和左值引用的概念类似,也就是能够指向右值但不能指向左值的引用。为了和左值引用区别, 使用`&&`也就是两个`&`符。老实讲这个符号很令人费解,因为它和`and`的含义相同。 52 | 53 | ```C++ 54 | int a = 4; 55 | int &&b = 4; // 合法 56 | int &&c = a; // 非法 57 | ``` 58 | 59 | 上面第三行代码非法的原因是 c 是一个右值引用,它不能指向左值。如果我们非要指向呢?也不是没有办法,可以使用`std::move`函数,它可以将一个左值转换成右值。 60 | 61 | ```C++ 62 | using namespace std; 63 | int a = 4; 64 | int &&c = move(a); 65 | ``` 66 | 67 | `move`函数听起来似乎是移动的意思,但其实它并没有移动变量,只不过做了一个类似于类型转换的操作。 68 | 69 | 不知道大家看到这里有没有觉得头大,其实还没有结束,还有一点很重要。即左值引用和右值引用这两者本身都是左值引用: 70 | 71 | ```C++ 72 | void test(int && tmp) { 73 | tmp = 2333; 74 | } 75 | 76 | using namespace std; 77 | 78 | int a = 4; 79 | int &b = a; 80 | int &&c = 4; 81 | 82 | test(a); // 非法 83 | test(b); // 非法 84 | test(c); // 非法 85 | test(move(a));// 合法 86 | test(move(b));// 合法 87 | test(move(c));// 合法 88 | ``` 89 | 90 | C++中的引用是一个非常大的范畴,除了左值引用、右值引用之外还有非常多的细节。比如万能引用、引用折叠、完美转发等…… 91 | -------------------------------------------------------------------------------- /C++/39-函数模板.md: -------------------------------------------------------------------------------- 1 | ## 函数模板 2 | 3 | 所谓函数的模板,本质上也就是使用泛型来定义函数。 4 | 5 | 所谓的泛型其实也就是不定的类型,比如说我们使用`vector`的时候,可以定义各种类型的`vector`,可以定义存储`int`型的`vector`也可以定义存储`float`类型的,也可以定义存储其他类型。我们在声明的时候将存储的类型当做参数传给了模板。 6 | 7 | 泛型可以用具体的类型,比如(`int`或`double`)替换,通过将类型作为参数传给模板,编译器会根据传递的参数类型生成该类型的函数。这种方式也被称为通用编程或者参数化类型。 8 | 9 | 举一个很简单的例子,比如说我们要实现一个函数交换两个变量的值。对于`int`类型我们要实现一遍,对于`double`类型我们又要实现一遍,如果还需要其他类型,那么又需要额外实现很多同样逻辑的函数。当然可以拷贝代码,但显然这样会很浪费时间,而且会使得代码变得臃肿。 10 | 11 | 这个时候我们就可以使用函数模板自动完成这一功能,函数模板允许以任意类型来定义函数,所以我们就可以这样实现: 12 | 13 | ```C++ 14 | template 15 | void swap(T &a, T &b) { 16 | T temp = a; 17 | a = b; 18 | b = temp; 19 | } 20 | ``` 21 | 22 | 当我们要创建一个模板的时候,需要首先声明模板的类型,也就是`template`语句做的事情。关键字`typename`也是必须的,也可以使用`class`代替。`typename`关键字是在 C++98 标准添加的,所以在更早的版本中往往使用`class`。在这个场景下,这两种方式是等价的。C++ Primer 当中更建议使用`typename`而非`class`。 23 | 24 | `typename`之后跟的是类型的名称,我们可以使用任意的名字,一般来说习惯性地会使用字母`T`。我们在使用的时候和普通函数并没有什么不同,当做普通函数使用即可。 25 | 26 | ```C++ 27 | template 28 | void swap(T &a, T &b) { 29 | T temp = a; 30 | a = b; 31 | b = temp; 32 | } 33 | 34 | int i = 10, j = 20; 35 | swap(i, j); 36 | double a = 3.0, b = 4.0; 37 | swap(a, b); 38 | ``` 39 | 40 | 虽然我们只实现了一次函数,但是在编译的时候,编译器会将这个函数根据我们使用的情况生成多个版本。比如在上面的代码当中,我们使用了`int`和`double`两种类型的函数。编译器会替我们生成两份代码,也就是说最终代码上和我们手动实现函数重载是一样的,可以理解成一种方便我们程序编写的特性。 41 | -------------------------------------------------------------------------------- /C++/4-整型.md: -------------------------------------------------------------------------------- 1 | ### 整型 2 | 3 | 整型即整数,与小数对应。 4 | 5 | 许多语言只能表示一种整型(如 Python),而在 C++当中根据整数的范围提供了好几种不同的整型。 6 | 7 | C++的基本整型有 char、short、int、long,在 C++ 11 标准中,新增了 long long。在部分编译器当中不支持 long long,而支持\_\_int64。稍后会有单独的文章对此进行解释和补充说明。 8 | 9 | 其中 char 类型有一些特殊属性,通常被用来当做字符而非整数。另外,每一种类型都有有符号版本和无符号版本两种,所以总共一共有 10 种类型。 10 | 11 | ### short、int、long 和 long long 12 | 13 | 这四种类型都是整型,唯一的不同是范围的区别。受到底层硬件的影响,C++当中这四种类型的范围并不是固定的。由于要兼容各种不同类型的系统与硬件,所以没有办法对类型进行统一。 14 | 15 | 为了避免引起不便,C++提供了一种灵活的标准,它确保了每一种类型的最小范围。 16 | 17 | - short 至少 16 位 18 | - int 至少与 short 一样长 19 | - long 至少 32 位,且至少与 int 一样长 20 | - long long 至少 64 位,且至少与 long 一样长 21 | 22 | ### 位与字节 23 | 24 | 计算机内存的基本单元是位,英文是 bit,音译成比特。一位 bit 只有 0 和 1 两个值,可以将其看成是开关。8 位 bit 一共有 256 中不同的组合,即$2^8=256$。因此 8 位 bit 可以表示 0-255 或者-128-127。 25 | 26 | 每增加一个二进制位,可以表示的范围翻倍。因此 16 位可以表示 65536 个值,而 32 位可以表示 4294672296 个值,64 位更大,大约能表示$1.8 * 10^{19}$。这个范围足够表示银河系中所有的星星。 27 | 28 | 8 位二进制位是一个字节(byte),字节是计算机存储的最小计量单位。1024 个字节称为 1KB,而 1024 个 KB 又被称为 1MB,1024MB 为 1GB。 29 | 30 | 一般在操作系统当中,都有最小长度,这通常是由 CPU 的位数所决定的。在常用的操作系统当中如 Linux、Windows、MacOS,int 和 long 为 32 位,short 为 16 位,而 long long 为 64 位。 31 | 32 | 除了根据通常情况来推测之外,C++当中也提供了一些现成的工具来查看。比如 sizeof 函数,sizeof 函数可以查看变量占据的字节数。这个函数既可以接受变量类型也可以接受变量本身,如果传入的是变量类型,那么计算的结果就是该类型的变量占据的内存大小,同理如果是变量本身,则表示变量本身占据的内存。 33 | 34 | 需要注意的是,当我们查看对象是变量类型时,需要使用括号,如果是变量本身,则括号是可选的。 35 | 36 | ```C++ 37 | cout << sizeof(int) << endl; 38 | int a = 3; 39 | cout << sizeof a << endl; 40 | ``` 41 | 42 | 上述两个 cout 的输出结果都是 4。 43 | 44 | 除了 sizeof 函数之外,C++还提供了大量的常量。比如`INT_MAX`,`LONG_MAX`等,顾名思义这些常量的值就是各个类型的最大值。C++ primer 当中说这些常量存储在头文件 climits 当中,但 iostream 等包都会间接引入它,所以编码的时候无须特地引入。 45 | 46 | 有最大值,也一样有最小值,如`INT_MIN`,`LONG_MIN`等。我个人感觉这块使用频率不高,就不过多赘述了,有需要去翻阅一下 primer 即可。 47 | 48 | ### 初始化 49 | 50 | 我们之前在介绍变量的时候只是介绍了声明变量的方式,类似于: 51 | 52 | ```C++ 53 | int a, b; 54 | ``` 55 | 56 | 但其实我们可以把变量的声明语句与赋值语句结合在一起,在声明的同时进行初始化。例如: 57 | 58 | ```C++ 59 | int a = 3; 60 | char c = 'c'; 61 | ``` 62 | 63 | 当然这个只是最基础的初始化方式,尤其是后续涉及到面向对象时还有更多的使用细节。 64 | 65 | ### 无符号类型 66 | 67 | 前文当中在介绍位和字节的时候曾经提到过,比如 8 位二进制位既可以表示 0-255 也可以表示-128-127。这其实就是有符号和无符号的区别。 68 | 69 | 如果我们确定我们要存储的整数为非负数,并且想要拥有更大的范围,就可以使用无符号修饰符 unsigned 来修饰这几种类型。比如: 70 | 71 | ```C++ 72 | unsigned short ushort; 73 | unsigned int uint; 74 | unsigned long ulong; 75 | unsigned long long ull; 76 | ``` 77 | 78 | 其中`unsigned int`可以简写成`unsigned`,其他的用法和有符号的整数是一样的。 79 | -------------------------------------------------------------------------------- /C++/40-重载模板.md: -------------------------------------------------------------------------------- 1 | ### 重载模板 2 | 3 | 函数模板可以使得同一个函数对不同类型使用,非常地方便。但有的时候类型不同,只是通过模板是没办法解决的, 可能逻辑上也会有所区别,这个时候只是使用模板是无法解决的。 4 | 5 | 为了满足这种需求,我们可以像是重载函数那样重载模板。和常规的函数一样,重载的模板的函数特征,也就是入参的数量和类型必须有所不同。 6 | 7 | 举个例子,比如我们之前定义了一个函数模板用来交换两个变量的值。如果我们要交换的不只是变量,而是两个数组,就必须要修改逻辑了。 8 | 9 | ```C++ 10 | template 11 | void Swap(T &a, T &b); 12 | 13 | template 14 | void Swap(T *a, T *b, int n); 15 | ``` 16 | 17 | 可以看到我们额外传入了一个`int` n,它表示数组的长度。另外,我们入参的类型也发生了变化,不再是模板类型`T`的引用,而是指针了。因为我们要接收的是一个数组,而数组在函数传递当中都是以指针的形式进行的。所以这里要写成指针,当然也可以写成这样:`T a[]`,两种形式本质上没有区别。 18 | 19 | 所以我们实现的话会是这样: 20 | 21 | ```C++ 22 | template 23 | void Swap(T &a, T &b) { 24 | T temp = a; 25 | a = b; 26 | b = temp; 27 | } 28 | 29 | template 30 | void Swap(T *a, T *b, int n) { 31 | for (int i = 0; i < n; i++) { 32 | Swap(a[i], b[i]); 33 | } 34 | } 35 | ``` 36 | 37 | ### 问题 38 | 39 | 到这里,相信大家也能看出一点问题。 40 | 41 | 假设我们有这样一个模板函数: 42 | 43 | ```C++ 44 | template 45 | void Swap(T a, T b); 46 | ``` 47 | 48 | 虽然理论上类型`T`是万能类型,什么类型都可以接受。但我们操作的时候会有很多问题,比如我们执行`a = b`,对于数组类型就会报错。 49 | 50 | 再比如我们执行`a > b`,很多类型也无法进行比较大小。再比如进行算术运算等等,很多类型比如指针、数组或者结构体也没办法进行算术运算。 51 | 52 | 总之模板的功能是很局限的,有的时候只能处理某些类型,很难通用覆盖所有情况。当然有的时候也是有一些其他办法绕开的,比如结构体也可以重载比较运算符,也可以重载一些算术运算符等等。 53 | 54 | 除此之外,C++当中也提供了另外的解决方案。由于篇幅的限制,我们下次再说~ 55 | -------------------------------------------------------------------------------- /C++/41-模板显式具体化.md: -------------------------------------------------------------------------------- 1 | ## 模板显式具体化 2 | 3 | 前文当中说了,模板函数虽然非常好用,但是也存在一些问题。比如有的操作并不是对所有类型都适用的,针对这种情况 C++提供了一个解决方案,就是针对特定类型提供具体化的模板定义。这里的具体可以理解成类型的具体。 4 | 5 | 我们来看一个 C++ Primer 当中的例子,假设现在我们有一个结构体叫做 job: 6 | 7 | ```C++ 8 | struct job { 9 | string name; 10 | double salary; 11 | int floor; 12 | } 13 | ``` 14 | 15 | 对于结构体是可以整体赋值的,所以之前的`Swap`函数对它一样适用。 16 | 17 | ```C++ 18 | template 19 | void Swap(T &a, T &b) { 20 | T temp = a; 21 | a = b; 22 | b = temp; 23 | } 24 | ``` 25 | 26 | 但我们现在希望在交换结构体的时候,只是交换`salary`和`floor`这两个字段,把`name`保持不变。由于我们希望引入逻辑变化,所以直接调用`Swap`函数就不可行了。 27 | 28 | 当然我们可以不用函数模板,直接重载函数: 29 | 30 | ```C++ 31 | void Swap(job &a, job &b) { 32 | // swap为std自带的交换函数,在algorithm头文件中 33 | swap(a.salary, b.salary); 34 | swap(a.floor, b.floor); 35 | } 36 | ``` 37 | 38 | 由于 C++当中规定,非函数模板的优先级大于函数模板,所以我们在对`job`结构体调用`Swap`函数的时候,会优先使用这个。 39 | 40 | 除此之外,我们还可以提供一个具体化的模板函数: 41 | 42 | ```C++ 43 | template <> void Swap (job &a, job &b) { 44 | swap(a.salary, b.salary); 45 | swap(a.floor, b.floor); 46 | } 47 | ``` 48 | 49 | 这个函数的写法看起来有些特殊,我们在函数类型之前加上了`template <>`,在函数名后面又跟上了``。它表示的是这是一个函数模板的显式具体化,也可以理解成为之前的函数模板提供一个`job`类型的版本。C++当中规定显式模板函数的优先级高于普通模板函数。 50 | -------------------------------------------------------------------------------- /C++/42-模板实例化.md: -------------------------------------------------------------------------------- 1 | ## 实例化和具体化 2 | 3 | 关于函数模板,还有一个很重要的概念,就是实例化。 4 | 5 | 我们在编写代码时,如果只是编写了函数模板本身,编译器是不会为我们生成函数的定义的。当编译器使用模板为特定的类型生成函数定义时,就会得到一个模板的实例。这个概念有点像是 Python 里的元类,元类的实例是另外一个类。 6 | 7 | 比如我们定义了一个函数模板: 8 | 9 | ```C++ 10 | template 11 | void Swap(T &a, T &b) { 12 | T temp = a; 13 | a = b; 14 | b = temp; 15 | } 16 | ``` 17 | 18 | 当我们调用它,传入两个`int`类型的时候,编译器就会生成一个实例,这个实例使用的类型是`int`。当我们使用`double`类型的参数又一次调用的时候,编译器会继续生成`double`类型的实例。这个生成实例的过程是不可见的,所以被称为隐式实例化。 19 | 20 | 在早年的 C++版本当中只支持隐式实例化,但现在 C++允许显示实例化。也就意味着我们可以手动命令编译器创建特定的实例,比如`Swap()`。语法是通过`<>`声明指定模板类型,并且在声明之前加上关键字`template`,如: 21 | 22 | ```C++ 23 | template void Swap(int, int); 24 | ``` 25 | 26 | 这个语法看起来和显式具体化非常相似,显式具体化的写法是: 27 | 28 | ```C++ 29 | template<> void Swap(int &, int &); 30 | template<> void Swap(int &, int &); 31 | ``` 32 | 33 | 看起来非常相似,但是含义是完全不同的。显式具体化的含义是对于某特定类型不要使用原模板生成函数,而应专门使用指定的函数定义。而显式实例化是使用之前的模板函数的定义的,只不过是手动触发编译器创建函数实例而已。 34 | 35 | 对了,我们不能同时在一个文件中,使用同一种类型的显式实例化和显式具体化,这会引起报错。 36 | 37 | 我们如果死记显式实例化的声明,的确很容易和具体化混淆。但我们可以在代码当中直接使用,直接使用的形式则要简单许多,只需要通过`<>`表明类型即可。例如: 38 | 39 | ```C++ 40 | template 41 | T Add(T a, T b) { 42 | return a + b; 43 | } 44 | 45 | int main() { 46 | int a = 3; 47 | double b = 3.5; 48 | cout << Add(a, b) << endl; 49 | } 50 | ``` 51 | 52 | 在上面这段代码当中,我们通过给`Add`函数加上了``来手动创建了一个接受`double`类型的函数。需要注意的是,我们传入的`a`是一个`int`类型。所以编译器会执行强制类型转换,将它转换成`double`传入。 53 | -------------------------------------------------------------------------------- /C++/43-编写头文件.md: -------------------------------------------------------------------------------- 1 | ## 编写头文件 2 | 3 | 我们之前做的左右示例都是在一个单独的 cpp 文件当中完成的,当我们要做一个相对复杂或大型的项目时,我们显然不能把所有代码都写在一个 cpp 里。这就需要我们拆分代码,但代码按照逻辑划分,写入不同的 cpp 文件当中。 4 | 5 | 在我们编译的时候,我们可以将这些 cpp 文件分别单独编译,最后再连接到一起。这样做的好处是,当我们只修改了某一个文件的时候,可以只用单独编译那一个文件,不会影响其他文件的编译结果。一般来说大型项目,都会使用一下自动化的编译工具,比如`make`等,不会手动执行编译过程,但对于这其中的一些细节,还是需要有所了解。 6 | 7 | 我们来看 C++ primer 当中提供的一个例子。 8 | 9 | 现在我们要实现一个将直接坐标转化成极坐标的功能,我们需要定义两个结构体分别表示直角坐标和极坐标,另外还需要实现从直接坐标到极坐标的转化。 10 | 11 | 显然相对于主体程序而言,这部分代码是独立的,所以我们可以把它们放入一个单独的 cpp 文件当中。首先要明确的是,main()函数和其他函数都用到了同一个结构体,因此两个 cpp 文件都需要包含该结构体的声明。显然拷贝代码是很糟糕的选择,比较好的做法是将结构体的声明写在头文件当中,通过`#include`语句引入。 12 | 13 | 这样的话整体的代码就分成三个部分: 14 | 15 | - 头文件:包含结构体声明、函数声明 16 | - coordin.cpp:包含坐标系转化相关的代码 17 | - main.cpp:主体程序 18 | 19 | 在之后面向对象的章节当中, 我们将会经常用到这样的代码结构。 20 | 21 | 对于头文件当中的内容有严格的限制,由于头文件可能会被多个 cpp 文件引入,所以我们不能将函数的实现或参数的定义放入头文件当中。因为同一个程序中包含同一个函数的多个定义会引发报错,参数同理。 22 | 23 | 只有以下内容可以写入头文件当中: 24 | 25 | - 函数原型(函数声明) 26 | - `#define`或`const`定义的符号常量 27 | - 结构体声明 28 | - 类声明 29 | - 模板声明 30 | - 内联函数(inline) 31 | 32 | 在同一个文件当中只能引入一个头文件一次,但有的时候由于引用依赖的原因,可能会导致重复引入。比如引入 A 和 B 头文件,B 头文件中引入了 A,导致 A 被引入两次。 33 | 34 | 为了解决这个问题,我们可以加入预编译指令`#ifndef`,含义是 if not defined,判断某定义是否存在。只有当定义不存在时才会直接`#ifndef`和`#endif`之间的语句: 35 | 36 | ```C++ 37 | #ifndef COORDIN_H_ 38 | // statements 39 | #endif 40 | ``` 41 | 42 | 一般情况下我们使用`#define`创建符号常量: 43 | 44 | ```C++ 45 | #define MAXI 4096 46 | ``` 47 | 48 | 但由于这里我们只是用来区分是否引入,所以只需要名称即可: 49 | 50 | ```C++ 51 | #ifndef COORDIN_H 52 | #define COORDIN_H 53 | // todo 54 | #endif 55 | ``` 56 | 57 | 这样,当引入一次之后,`COORDIN_H`即被定义,那么下次就不会再执行这段代码。最后,我们写出完整的头文件代码: 58 | 59 | ```C++ 60 | #ifndef COORDIN_H__ 61 | #define COORDIN_H__ 62 | 63 | struct polar { 64 | double distance, angle; 65 | }; 66 | 67 | struct rect { 68 | double x, y; 69 | }; 70 | 71 | polar rect_to_polar(rect xpros); 72 | void show_polar(polar dapos); 73 | 74 | #endif 75 | ``` 76 | -------------------------------------------------------------------------------- /C++/44-联合编译.md: -------------------------------------------------------------------------------- 1 | ## 联合编译 2 | 3 | 在上一篇当中,我们编写好了头文件`coordin.h`,现在我们要完成它的实现。 4 | 5 | 头文件当中只能放一些生命和常量的定义,不能有具体的实现。所以我们要把具体的实现单独放入一个 cpp 文件当中。因为我们的头文件叫做`coordin.h`,那么我们与之对应的 cpp 文件自然就叫做`coordin.cpp`。 6 | 7 | 在`coordin.h`当中我们声明了两个函数,自然我们就要完成这两个函数的实现: 8 | 9 | ```C++ 10 | #include 11 | #include 12 | #include 13 | #include "coordin.h" 14 | 15 | using namespace std; 16 | 17 | polar rect_to_polar(rect xypos) { 18 | polar answer; 19 | answer.distance = sqrt(xypos.x * xypos.x + xypos.y * xypos.y); 20 | answer.angle = atan2(xypos.y, xypos.x); 21 | return answer; 22 | } 23 | 24 | void show_polar(polar dapos) { 25 | const double rad_to_deg = 57.29577951; 26 | 27 | cout << "distance = " << dapos.distance; 28 | cout << ", angle = " << dapos.angle * rad_to_deg; 29 | cout << " degress" << endl; 30 | } 31 | ``` 32 | 33 | 这两个函数一个完成的是直角坐标到极坐标的转换,还有一个是极坐标的输出,输出的时候还包括了一个弧度到角度的转化。 34 | 35 | 最后我们再来看`main`函数: 36 | 37 | ```C++ 38 | #include "coordin.h" 39 | using namespace std; 40 | 41 | int main() { 42 | rect rplace; 43 | polar pplace; 44 | while (cin >> rplace.x >> rplace.y) { 45 | pplace = rect_to_polar(rplace); 46 | show_polar(pplace); 47 | } 48 | } 49 | ``` 50 | 51 | 这里有一个小细节,我们在引入`coordin.h`的时候使用的是双引号,而不是`<>`符号。因为如果使用的是尖括号,那么 C++编译器将在存储标准头文件的文件系统当中去查找这个头文件,如果是双引号则会在当前目录或源代码目录查找。 52 | 53 | 还有虽然我们用到的函数实现是在`coordin.cpp`当中实现的,但我们并不需要将它`include`进来。而是在之后编译的时候连接进来。 54 | 55 | 现在我们的代码都已经写好了,但是我们有两个 cpp 文件,要怎么编译运行呢? 56 | 57 | 我们可以使用`g++ -c`命令,将 cpp 代码编译成目标代码。 58 | 59 | ```sh 60 | g++ -o coordin.cpp 61 | ``` 62 | 63 | 编译之后,我们将会得到一个`coordin.o`文件,我们再继续编译`main.cpp`文件: 64 | 65 | ```sh 66 | g++ -o main.cpp 67 | ``` 68 | 69 | 这样我们就得到了两个.o 文件,最后,我们需要把这两个.o 文件连接到一起编程一个可执行文件: 70 | 71 | ```sh 72 | g++ coordin.o main.o -o cur 73 | ``` 74 | 75 | 当然,我们也可以把`main.cpp`的编译和连接步骤合并在一起: 76 | 77 | ```sh 78 | g++ main.cpp coordin.o -o cur 79 | ``` 80 | 81 | 我们单独对每个文件编译的好处是,比如当我们只需要改动`main.cpp`的时候,`coordin.cpp`文件可以不用再编译,从而节省编译运行的时间。我们都知道,大型的 C++项目的编译是非常耗时的。 82 | 83 | 当然大型项目当中,我们一般也不会手动编译项目,而会使用例如`make`等一些自动编译脚本。 84 | -------------------------------------------------------------------------------- /C++/45-自动存储连续性.md: -------------------------------------------------------------------------------- 1 | ## 自动存储持续性 2 | 3 | 这个概念乍一看有些拗口,其实它很简单,指的是在函数定义中声明的变量的存储持续性是自动的:它们在程序开始执行其所属的函数或代码块时被创建,在执行完函数或代码块时,它们使用的内存被释放。 4 | 5 | 在默认情况下,我们在函数中声明的变量和函数的参数都是自动存储持续性,它的作用于为局部,没有链接性。 6 | 7 | 这里的链接性描述了名称如何在不同的单元之间共享,链接性为外部的名称可以在文件之间共享,链接性为内部的名称只能由一个车文件中的函数共享。自动变量的名称没有链接性,也就是不能共享。 8 | 9 | 这段话是一段专业性描述,看不太明白没有关系。简单理解,就是说如果我们在`main`函数当中声明了一个变量 a,并且在函数`test`当中也声明了一个同样的变量 a。两者之间是完全独立的,彼此不会有任何影响,因为它们只有在声明了它的函数中才能使用。 10 | 11 | 当程序执行`test`函数时,`test`中的 a 才被创建,当`test`执行结束,a 即被销毁。 12 | 13 | 其实变量的作用域的最小划分并不是函数,而是代码块。我们在代码块当中创建的变量,它的使用范围作用域就只有当前代码块。例如: 14 | 15 | ```C++ 16 | int main() { 17 | int a = 4; 18 | { 19 | int b = 10; 20 | } 21 | } 22 | ``` 23 | 24 | 我们在`main`函数当中定义了一个变量 a 和一个变量 b,但是这两个变量定义的位置不同。b 定义在了一个代码块(代码块使用花括号表示)当中,对于 a 来说,它的作用域是整个`main`函数,而 b 只在花括号当中生效。 25 | 26 | 更有意思的是下面这段代码: 27 | 28 | ```C++ 29 | int main() { 30 | int a = 4; 31 | { 32 | int a = 5; 33 | cout << a << endl; 34 | } 35 | cout << a << endl; 36 | } 37 | ``` 38 | 39 | 答案是输出的结果是 5 和 4,为什么呢? 40 | 41 | 因为内部和外部都有一个变量 a,当执行内部代码的时候,编译器隐藏了外部的定义。而程序离开代码块时,外部的定义又重新恢复。 42 | -------------------------------------------------------------------------------- /C++/46-auto关键字.md: -------------------------------------------------------------------------------- 1 | ### auto 关键字 2 | 3 | 我们现在知道,使用`auto`关键字可以自动推导变量类型,尤其是在一些很复杂的情况下,使用`auto`可以大大简化代码。但是在早期的 C++和 C 语言当中,`auto`关键字被用来显式地表示某个变量为自动存储。 4 | 5 | ```C++ 6 | auto float dis; 7 | ``` 8 | 9 | 这个功能几乎不被使用,所以在 C++11 当中进行了更新,赋予了它新的含义。 10 | 11 | ### 静态持续变量 12 | 13 | 除了自动存储变量之后,C++当中还有静态持续变量。关于静态持续变量的定义 C++和 C 语言是一样的,它拥有三种链接性,即**外部链接性、内部连接性和无链接性**。其中外部链接性指的是可以在其他文件中访问,内部链接性指的是只能在当前文件访问,无链接性指的是只能在当前函数或代码块中访问。 14 | 15 | 这三种链接性虽然范围不同,但都在整个程序运行期间存在,因此它们的寿命更长,由于在程序运行期间,静态持续变量的数目保持不变,因此不需要使用栈来管理它们。编译器将会分配固定的内存块来存储所有的静态变量,这些变量在整个程序运行期间一直存在。 16 | 17 | 并且所有没有显式初始化的静态变量,编译器都会将它置为 0。在默认情况下,静态数组和结构将所有元素和成员的所有位都置为 0。 18 | 19 | 下面介绍一下这三种静态持续变量的创建方法: 20 | 21 | ```C++ 22 | int cnt = 1000; 23 | static int one_file = 50; 24 | 25 | void func1(int n) { 26 | static int ret = 0; 27 | } 28 | 29 | int main() { 30 | // some statements 31 | } 32 | ``` 33 | 34 | 在上面这段代码当中,我们定义了三个静态持续变量:`cnt`,`one_file`,`ret`。其中`cnt`为外部链接性,它可以在任何地方使用。`one_file`为内部链接性,可以在当前文件中任意地方使用。而`ret`没有链接性,只能在函数`func1`中使用。 35 | 36 | 但是这里有一点要注意,虽然`ret`只能在函数`func1`中使用,但这并不意味着函数`func1`执行之前`ret`不存在。前面说了静态持续变量拥有单独的内存块来存储,并不受函数生命周期的影响。我们来看一个例子: 37 | 38 | ```C++ 39 | void test() { 40 | static int ret = 0; 41 | ret++; 42 | cout << ret << endl; 43 | } 44 | 45 | int main() { 46 | test(); 47 | test(); 48 | return 0; 49 | } 50 | ``` 51 | 52 | 请问当我们运行上述代码,屏幕当中会得到什么结果? 53 | 54 | 答案是 1 和 2,因为`ret`变量是一个静态持续类型,虽然我们只能在函数`test`内部使用它,但它不受函数`test`生命周期的影响。也就是说`static int ret = 0;`这句初始化语句只会在`test`函数第一次执行时执行一次,之后即使函数退出,这个值依然存在。当我们第二次执行的时候,由于`ret`变量已经存在,所以并不会将它再置为 0。 55 | 56 | 对于这里用到的关键字`static`,简单理解成静态是不妥的。其实它有两层含义,当我们是在一个函数内部局部声明的时候,它表示的是这个变量是一个无链接性的静态变量,表示的是存储的持续性。当用于代码块外部声明时,`static`表示的是内部链接性,如果不加`static`则表示外部链接性。所以这其中的作用是有一点区别的,也有人将它称为关键字重载,即关键字的具体含义取决于上下文。 57 | -------------------------------------------------------------------------------- /C++/47-全局变量.md: -------------------------------------------------------------------------------- 1 | ## 全局变量 2 | 3 | 前面聊到了静态持续变量的链接性,其中链接性为外部的变量通常简称为外部变量。它们的存储持续性为静态,作用域为整个程序。外部变量是在函数外部定义的,因此对于所有的函数而言都是外部的。因此外部变量也被称为全局变量。 4 | 5 | 关于外部变量,C++当中有一个单定义规则:变量只能有一次定义。 6 | 7 | 这句话看起来有些难以理解,为了理解它,我们需要厘清 C++当中的两种变量声明。一种是定义声明简称为定义,它给变量分配存储空间。另外一种是引用声明,简称为声明,它不给变量分配存储空间。因为它表示引用一个已有的变量。 8 | 9 | 引用声明使用关键字`extern`,并且不进行初始化。否则会被视为是定义,而非声明。 10 | 11 | ```C++ 12 | double up; 13 | extern int blue; 14 | extern char ch = 'z'; 15 | ``` 16 | 17 | 上面的例子当中,第一行和第三行为定义声明,因为第一行没有加关键字`extern`,而第三行的变量进行了初始化。 18 | 19 | 可能到这里还是有些难以理解,其实`extern`是为了引入其他文件创建的变量。如果我们不加`extern`,表示在当前文件当中创建一个全局变量,而加上了`extern`表示引入其他文件创建的全局变量。外部链接性的全局变量虽然是整个程序都可使用的,但跨文件使用时,需要手动使用`extern`声明。 20 | 21 | 没有通过`extern`声明的其他文件的全局变量也是无法使用的。 22 | 23 | 当我们同时使用局部变量和全局变量的时候会发生什么呢?比如下面这段代码: 24 | 25 | ```C++ 26 | // file1 27 | double warning = 3.0; 28 | 29 | // file2 30 | 31 | extern double warning; 32 | 33 | void test() { 34 | double warning = 5.0; 35 | cout << warning << endl; 36 | } 37 | ``` 38 | 39 | 在这段代码当中, 我们在`file1`中创建了变量`warning`,在`file2`当中声明了这个全局变量。并且在`test`函数当中又定义了一个新的`double`类型的变量也叫做`warning`,那么问题来了,当我们使用`cout`输出的时候,究竟得到的结果会是什么呢? 40 | 41 | 答案很简单,得到的结果是 5.0。因为当同时使用同名的局部变量和全局变量时,局部变量将隐藏全局变量。 42 | 43 | 那么全局变量和局部变量究竟应该使用哪一种呢? 44 | 45 | 很明显,全局变量非常方便,使用全局变量可以免去很多参数传递的过程,大大简化了编码的难度。但如果是在大型的工程当中,使用全局变量则是一个非常危险的行为。因为全局变量人人都能访问,我们很难保证数据的准确性,会不会遭遇不可知的更改,排查的时候也更加困难。而使用局部变量,参数传递的链路是清晰的,debug 的时候会容易许多。 46 | 47 | 在我们日常的算法联系当中,并不存在数据不可靠的问题。使用全局变量可以简化许多数据结构的实现过程,尤其是一些相对复杂的数据结构。我认识的所有算法竞赛的大牛,都非常喜欢使用全局变量。所以到底该用哪个并不是固定的,要根据我们的实际需要进行选择。 48 | -------------------------------------------------------------------------------- /C++/48-内部链接性.md: -------------------------------------------------------------------------------- 1 | ## 内部链接性 2 | 3 | 当我们使用`static`关键字,将变量的作用于限制在整个文件时,该变量的链接性为内部链接性。在多文件的程序当中,内部链接性和外部链接性是有所差别的,内部链接性的变量只能在当前文件使用,而常规外部变量是可以跨文件使用的。 4 | 5 | 但这就带来了一个问题,如果我们在两个文件当中都定义了同一个全局变量,会发生什么呢? 6 | 7 | ```C++ 8 | // file1 9 | int error = 20; 10 | 11 | // file2 12 | int error = 5; 13 | ``` 14 | 15 | 答案是,这样的做法会报错,因为它违反了单定义规则。 16 | 17 | 但是我们可以将其中一个变量通过`static`关键字变成静态内部变量,那么在该文件当中,静态变量将隐藏常规的外部变量: 18 | 19 | ```C++ 20 | // file1 21 | int error = 20; 22 | 23 | // file2 24 | static int error = 5; 25 | ``` 26 | 27 | 这样做的好处是,当我们确定我们某些变量的使用范围只有当前文件的时候,我们可以使用`static`关键字来防止它和一些其他文件当中的外部变量相冲突。 28 | 29 | ## 无链接性 30 | 31 | 无链接性的变量我们在之前的文章当中也曾经提到过,其实就是在代码块当中使用`static`关键字创建的。 32 | 33 | 在代码块当中创建的静态变量会和代码块的生命周期脱钩,虽然它只能在代码块运行时使用,但它的结果会一直存在,并不会随着代码块的执行结束而消亡。 34 | 35 | ```C++ 36 | void test() { 37 | static int ret = 0; 38 | ret++; 39 | cout << ret << endl; 40 | } 41 | 42 | int main() { 43 | test(); 44 | test(); 45 | return 0; 46 | } 47 | ``` 48 | 49 | 这段代码的运行之后将会得到 1 和 2,因为`ret`是一个无链接性的静态变量,它会一直存在。所以当`test`运行一次之后,它的结果会变成 1,并被保存下来。 50 | 51 | 主要应用场景就是我们希望有些变量能够随着代码块的运行有所沉淀,记录下中途的状态或者是中间结果。有了静态变量就可以不必使用全局变量了。 52 | -------------------------------------------------------------------------------- /C++/49-函数和语言链接性.md: -------------------------------------------------------------------------------- 1 | ## const 2 | 3 | 关于`const`的含义和使用我们之前已经讨论过了,但`const`限定符对于默认存储类型是有影响的。 4 | 5 | 在默认情况下,全局变量的链接性是外部的,也就是说所有文件均可使用。但`const`全局变量的链接性是内部的,也就是说只能在本文件当中使用。全局`const`定义就好像使用了`static`说明符一样。 6 | 7 | 这个设定看起来有些令人费解,其实是编译器中的一个优化。因为我们常常将一组常量放在头文件当中,并且在多个文件当中`include`这个头文件。如果`const`声明的链接性也是外部的,根据单定义规则,这会出错。正是为了避免这种情况发生,所以编译器做了优化,规定了`const`关键字修饰的全局变量,链接性也为内部。 8 | 9 | 如果出于某种原因,程序员希望某个常量的链接性为外部的,可以使用`extern`关键字来覆盖默认的内部链接性: 10 | 11 | ```C++ 12 | extern const int states = 30; 13 | ``` 14 | 15 | 在这种情况下,必须所有使用了该常量的文件中使用`extern`关键字来声明它。然而,由于`const`在多个文件之间共享,因此只有一个文件可以对它进行初始化。 16 | 17 | ## 函数和链接性 18 | 19 | 和变量一样,函数也有链接性。和 C 语言一样,C++不允许在一个函数当中定义另外一个函数,因此所有函数的存储持续性都默认是静态的,即在整个程序运行期间都一直存在。 20 | 21 | 在默认情况下,函数的链接性是外部的,也就是说可以跨文件使用。我们可以使用关键字`static`来讲函数的链接性设置成内部的,这样就只有当前文件可以使用。 22 | 23 | 我们必须同时在原型和定义当中使用`static`: 24 | 25 | ```C++ 26 | static void test(); 27 | 28 | static void test() { 29 | return ; 30 | } 31 | ``` 32 | 33 | 单定义规则对于函数也一样适用,也就是说对于非内联性函数来说,程序只能包含一个定义。对于链接性为外部的函数,只有一个文件包含该函数的定义。但使用该函数的每个文件,都应该包含函数的原型。 34 | 35 | 简单来说,这就是为什么我们通常将函数的原型写在头文件当中,而实现单独放在另外一个 cpp 文件中的原因。因为这样,所有`include`该头文件的文件都包含了函数的原型,但只有对应的`cpp`文件拥有函数的定义。这样就保证了一定不会违反单定义规则。 36 | 37 | ## 语言链接性 38 | 39 | 背景知识:链接程序要求每个不同的函数拥有不同的符号名,比如 C 语言编译器可能将`spiff`函数翻译成`_spiff`,这被称为 C 语言链接性。而 C++当中,同一个名称可以对应多个函数(函数重载),因此必须将这些函数翻译成不同的名称。 40 | 41 | 比如`spiff(int)`可能翻译成`_spiff_i`,`spiff(double, double)`翻译成`_spiff_d_d`。这种方法被称为 C++语言链接。 42 | 43 | 如果要在 C++程序当中使用 C 库中预编译的函数可能就会名称对不上,所以为了解决这个问题,我们可以用函数原型来指出要使用的约定: 44 | 45 | ```C++ 46 | extern "C" void spiff(int); 47 | extern void spoff(int); 48 | extern "C++" spaff(int); 49 | ``` 50 | 51 | 第一个原型使用的 C 语言链接性,后面两个使用的都是 C++语言链接性。只不过第二个是通过默认方式实现的,而第三个是显式指出的。 52 | -------------------------------------------------------------------------------- /C++/5-long long与__int64.md: -------------------------------------------------------------------------------- 1 | ### long long 和 \_\_int64 2 | 3 | 在 C++ Primer 当中提到的 64 位的 int 只有`long long`,但是在实际各种各样的 C++编译器当中,64 位的 int 一直有两种标准。一种是`long long`,还有一种是`__int64`,非主流的 VC 甚至还支持`_int64`。 4 | 5 | 对于一般的 C++开发者来说,其实这个问题不那么要紧,因为在实际开发当中,绝大多数情况使用 32 位的 int 就足够应付了。很少会出现超过 int 范围的情况,但是对于算法玩家来说,这是一个必须考量的问题。因为很多题目会故意把范围弄得很大,考察选手对于数据范围的敏感。 6 | 7 | 关于`long long`和`__int64`,我们有非常多的问题要讨论,我们一个一个来说。 8 | 9 | ### 历史遗留问题 10 | 11 | 首先是聊聊这个问题的背景,为什么会有两种标准呢?这并不是 C++的标准不严谨,或者是各大编译器乱来,背后是有一个历史遗留问题的。 12 | 13 | `long long`最早是 C99 标准引进的,然而 VC6.0 推出于 1998 年,在 C99 标准之前。所以当时微软就自己搞出来一个变量叫做`__int64`来表示 64 位整数。很多同学使用的第一个 C++的编译器就是 VC6.0,所以记得在 VC6.0 当中要使用`__int64`而非`long long`。 14 | 15 | 既然 VC6.0 搞出了`__int64`,那么微软后续的 C++版本显然就必须要兼容它。所以在 win 系统当中,这个`__int64`的变量类型就一直沿用了下来。当然,由于 C++标准的更新,当然最新的 visual studio 已经支持 long long 了。 16 | 17 | GCC 并不是基于 windows 系统的,自然支持`long long`。win 平台下的一些其他 IDE 如 dev C++,CodeBlocks 等也支持`long long`,因为它们为了和微软的系统兼容,所以也支持`__int64`。所以一个比较简单的区分方法是,判断编译器运行的操作系统是否是 windows,如果是 windows 使用`__int64`,否则使用`long long`。 18 | 19 | ### cin、cout 和 scanf、printf 的选择问题 20 | 21 | 这个问题对于 C++开发工程师来说同样不是个问题,没有任何选择的必要,无脑用 cin、cout 就完事了。但对于算法竞赛玩家来说,这依然是一个要考虑的问题。 22 | 23 | 因为在算法竞赛当中,尤其是当数据量很大的时候,读入和输出占据的时间是非常可观的。看起来只是`cin` `cout`和`scanf`和`printf`的差别,但是两者的性能差异非常大。 24 | 25 | 我曾经做过实验,同样的数据,使用`scanf`和`printf`的效率大约是`cin`、`cout`的十倍以上。在小数据量的时候当然没有差别,但数据量很大的时候影响非常大。很有可能导致同样的题目,同样的算法,别人通过了,但是我们却超时了的情况。 26 | 27 | 关于性能差异的原因,主要有两种解释。一种解释是说 cin 为了与 scanf 混用,而不用担心指针混乱,加上了绑定,总是会与 stdin 保持同步。正是这一步操作消耗了大量的时间。同理,cout 也会有类似的问题。第二种解释是 cout 在输出之前会把要输出的内容先存入缓存区,中间多了一个步骤,也会带来性能的降低。 28 | 29 | 关于 cin 与 stdin 同步带来的开销,我们是有办法解决的,只需要在加上这一行代码: 30 | 31 | ```C++ 32 | std::ios::sync_with_stdio(false); 33 | ``` 34 | 35 | 这行代码的意思是取消`cin`、`cout`与`stdin`、`stdout`的指针同步,会使得 cin、cout 的性能大大提升,达到和`scanf`、`printf`相差无几的程度。当然,更好的方法是使用`scanf`、`printf`代替。 36 | 37 | 而要使用`scanf`和`printf`又有一个问题,它们是 C 语言的标准输入输出方式,需要提供标识符来代表变量的类型,那么问题来了`long long`和`__int64`的标识符是什么呢? 38 | 39 | 这个其实一查就知道了,`long long`的标识符是 lld,所以我们使用 scanf 读入一个`long long`类型的数写成: 40 | 41 | ```C++ 42 | long long a; 43 | scanf("%lld", &a); 44 | ``` 45 | 46 | `__int64`的标识符是`I64d`,注意这里是大写的 i,不是 l。 47 | 48 | ```C++ 49 | __int64 a; 50 | scanf("%I64d", &a); 51 | ``` 52 | 53 | 但是这里面有一个很大的坑点,前面说了,目前在 windows 平台的编译器已经兼容了`long long`类型。但是即便如此,在 2013 之前的版本当中,我们输出的时候还是要使用`%I64d`,这是因为微软提供的 msvcrt.dll 库只支持`%I64d`的方式。相当于从底层上断绝了使用`%lld`输出的可能。2013 之后的版本里,微软修复了这个问题,添加了对`%lld`的支持。 54 | 55 | 所以比较简单的区分方法就是看操作系统,如果是 windows 系统,那么一律使用`__int64`准没错。如果是 linux 或者是 Mac 系统,那么统一使用`long long`。 56 | 57 | 我在网上找到了大神做的总结表,也可以直接参考下表: 58 | 59 | | 变量定义 | 输出方式 | gcc(mingw32) | g++(mingw32) | gcc(linux i386) | g++(linux i386) | MicrosoftVisual C++ 6.0 | 60 | | :-------- | :----------- | :----------- | :----------- | :-------------- | :-------------- | :---------------------- | 61 | | long long | “%lld” | 错误 | 错误 | 正确 | 正确 | 无法编译 | 62 | | long long | “%I64d” | 正确 | 正确 | 错误 | 错误 | 无法编译 | 63 | | \_\_int64 | “lld” | 错误 | 错误 | 无法编译 | 无法编译 | 错误 | 64 | | \_\_int64 | “%I64d” | 正确 | 正确 | 无法编译 | 无法编译 | 正确 | 65 | | long long | cout | 非 C++ | 正确 | 非 C++ | 正确 | 无法编译 | 66 | | \_\_int64 | cout | 非 C++ | 正确 | 非 C++ | 无法编译 | 无法编译 | 67 | | long long | printint64() | 正确 | 正确 | 正确 | 正确 | 无法编译 | 68 | 69 | > 参考资料:[博客:C/C++的 64 位整型](https://byvoid.com/zhs/blog/c-int64/) 70 | -------------------------------------------------------------------------------- /C++/50-存储方案和动态分配.md: -------------------------------------------------------------------------------- 1 | ## 存储方案和动态分配 2 | 3 | 在之前的文章当中,我们讨论了 C++用来为变量分配内存的 5 种方案,但是这些方案并不适用于使用`new`运算符分配的内存,这种内存被称为动态内存。 4 | 5 | 我们在之前的文章当中也曾介绍过,动态内存由`new`和`delete`控制,而不是由作用域和链接性规则控制。所以我们可以在一个函数当中分配动态内存,在另外一个函数中释放。 6 | 7 | 通常 C++编译器当中有三块独立的内存,一块用于静态变量,一块用于自动变量,还有一块用于动态存储。 8 | 9 | 虽然存储方案的概念不适用于动态内存,但是适用于动态内存的自动和静态指针。C++ Primer 当中有这么一个例子,我们在一个函数当中有如下语句: 10 | 11 | ```C++ 12 | float * p_fees = new float[20]; 13 | ``` 14 | 15 | 很明显,我们通过`new`创建了一个长度为 20 的 float 数组,这块数组的内存将会一直停留在内存当中,直到使用`delete`语句释放。但当该函数运行结束的时候,`p_fees`这个指针将会消失。如果希望在其他地方能够使用这个数组,需要将地址通过某种方式返回或者传递。 16 | 17 | 如果我们将`p_fees`的链接性声明为外部的,那么我们在其他地方都可以访问到了,如果另外的文件当中需要访问,也同样可以使用关键字`extern`。 18 | 19 | ### 初始化 20 | 21 | 前面讲了使用`new`申请内存的方法,如果我们想要在分配内存的同时完成变量的初始化,应该怎么办呢? 22 | 23 | 如果要为内置的标量类型分配空间并初始化,可以在类型名后面加上初始值,并将它用括号括起来: 24 | 25 | ```C++ 26 | int *pi = new int(3); 27 | double *pd = new double(99.9); 28 | ``` 29 | 30 | 如果我们要初始化结构体或者是数组,则需要使用大括号的列表初始化,这需要编译器支持 C++11,这是 C++11 中的新特性: 31 | 32 | ```C++ 33 | struct P { 34 | int x, y; 35 | }; 36 | 37 | P *p = new P{3, 4}; 38 | int *arr = new int[4] {2, 3, 4, 5}; 39 | ``` 40 | 41 | 在 C++11 当中也支持对单值变量使用列表初始化: 42 | 43 | ```C++ 44 | double *pd = new double(99.99); 45 | ``` 46 | -------------------------------------------------------------------------------- /C++/51-名称空间.md: -------------------------------------------------------------------------------- 1 | ## 名称空间 2 | 3 | 在 C++当中,名称可以是变量、函数、结构体、枚举、类以及结构体和类的成员。这本身并没有问题,但随着项目的增大,名称之间相互冲突的可能性也会大大增加。 4 | 5 | 比如我们使用了多个厂商的代码,它们都定义了`List`,`Tree`和`Node`类,但定义的方式不同,也就没办法互相兼容。这个时候当我们希望使用一个库的`List`类,而使用另外一个的`Tree`类,就会非常麻烦。这类冲突被称为名称空间(namespace)问题。 6 | 7 | ### 传统 C++名称空间 8 | 9 | 我们先来复习一下几个术语。 10 | 11 | - 声明区域 12 | 13 | 声明区域指的是可以在其中进行声明的区域,比如我们可以在函数外侧声明全局变量,对于全局变量,它的声明区域就是其声明所在的文件。对于函数中声明的变量, 它的声明区域就是其声明所在的代码块。 14 | 15 | - 潜在作用域 16 | 17 | 潜在作用域的范围比声明区域更加精确,它从声明语句处开始一直到声明区域的结尾。这是因为变量必须定义之后才能使用,所以潜在作用域的范围比声明区域要小。 18 | 19 | 这里有一个细节,变量并不一定在整个潜在作用域都是可见的。因为可能还会被嵌套在声明区域中的同名变量隐藏。比如说我们同时定义了一个全局变量和一个函数中的同名变量,那么在函数当中,外侧的全局变量将会被同名的局部变量隐藏。 20 | 21 | - 作用域 22 | 23 | 结合前面所说的,变量对于程序而言可见的范围被称为作用域,它又比潜在作用域更加精确一些。 24 | 25 | ### 新的名称空间特性 26 | 27 | C++新增了通过定义一种新的声明区域来创建命名的名称空间,这样做的目的是提供一个声明名称的区域。一个名称空间中的名称不会与另外一个名称空间的相同名称发生冲突,同时允许程序的其他部分使用该名称空间中声明的东西。 28 | 29 | 比如 C++ Primer 当中的这个例子,下面使用新的关键字`namespace`创建了两个名称空间 A 和 B。 30 | 31 | ```C++ 32 | namespace A { 33 | double pail; 34 | void fetch(); 35 | int pal; 36 | struct Well {...}; 37 | } 38 | 39 | namespace B { 40 | double bucket(double n) {...} 41 | double fetch; 42 | int pal; 43 | struct Hill {...}; 44 | } 45 | ``` 46 | 47 | 名称空间可以是全局的,也可以位于另外一个名称空间中,但不能位于代码块中。因此,默认名称空间里的所有声明的名称的链接性都是外部的,`const`关键字修饰的常量除外。 48 | 49 | 除了用户定义的名称空间之外,还存在另外一个名称空间——全局名称空间。它对应于文件级的声明区域,因此前面所说的全局变量现在被描述为位于全局名称空间中。 50 | 51 | 任何名称空间中的名称都不会与其他空间的名称发生冲突,因此 A 中的`fetch`可以和 B 中的`fetch`共存。名称空间中的声明和定义规则桶全局声明和定义的规则相同。 52 | 53 | 名称空间是开放的,可以把名称加入到已经创建的名称空间中,比如: 54 | 55 | ```C++ 56 | namespace A { 57 | char *goose(const char *); 58 | } 59 | ``` 60 | 61 | 同样我们之前在名称空间 A 当中只是定义了函数`fetch`,而没有定义,我们也可以在之后的代码当中添加定义: 62 | 63 | ```C++ 64 | namespace A { 65 | void fetch () { 66 | ... 67 | } 68 | } 69 | ``` 70 | 71 | 当然而我们需要一种方法来访问给定名称空间里的名称,最简单的方法是使用作用域解析符`::`,使用名称空间名来找到该名称: 72 | 73 | ```C++ 74 | A::pail = 12.34; 75 | A::fetch(); 76 | ``` 77 | 78 | 没有作用域解析符的名称成为未限定名称,包含了名称空间的名称称为限定的名称。 79 | 80 | 这一篇当中涉及了许多概念,看起来有些晦涩。但我个人感觉,这些概念理解起来并不复杂,主要是一些说明性的语言读起来有些难以理解。最好的办法就是沉下气来,一点点精读,先把前面理解了再看后面。 81 | -------------------------------------------------------------------------------- /C++/52-using声明.md: -------------------------------------------------------------------------------- 1 | ## using 声明 2 | 3 | C++当中提供了两种机制(using 声明和 using 编译指令)来简化对名称空间中名称的使用。using 声明使特定的标识符 keys,using 编译指令使整个名称空间可用。 4 | 5 | using 声明由关键字`using`和被限定的名称组成: 6 | 7 | ```C++ 8 | using A::fetch; 9 | ``` 10 | 11 | using 声明将特定的名称添加到它所属的声明区域中,完成声明之后,我们可以使用`fetch`代替`A::fetch`了。 12 | 13 | 我们来看一个具体的例子: 14 | 15 | ```C++ 16 | namespace A { 17 | double bucket(double n) {..} 18 | double fetch; 19 | struct Hill {...}; 20 | } 21 | 22 | int main () { 23 | using A::fetch; 24 | cin >> fetch; 25 | fetch += 1.0; 26 | A::Hill hill; 27 | } 28 | ``` 29 | 30 | 如果我们在函数外使用 using 声明,会将名称添加到全局名称空间中: 31 | 32 | ```C++ 33 | using A::fetch; 34 | 35 | void test() { 36 | cout << fetch << endl; 37 | } 38 | 39 | int main () { 40 | cin >> fetch; 41 | fetch += 1.0; 42 | A::Hill hill; 43 | } 44 | ``` 45 | 46 | ## using 编译指令 47 | 48 | using 声明只能使一个名称可用,而 using 编译指令可以使得所有的名称都可用。using 编译指令由名称空间和它前面的`using namespace`组成,它使得名称空间中的所有名称都可用。 49 | 50 | 如: 51 | 52 | ```C++ 53 | using namespace A; 54 | ``` 55 | 56 | 在全局声明区域使用 using 编译指令,将使得该名称空间的名称全局可用。这种情况其实我们已经非常熟悉了,因为我们一直在用`using namespace std`。 57 | 58 | 我们也可以在函数当中使用 using 编译指令: 59 | 60 | ```C++ 61 | int main () { 62 | using namespace A; 63 | } 64 | ``` 65 | -------------------------------------------------------------------------------- /C++/53-using声明和using指令.md: -------------------------------------------------------------------------------- 1 | ## using 声明 vs using 编译指令 2 | 3 | 使用 using 编译指令导入一个名称空间中所有的名称与使用多个 using 声明是不同的。 4 | 5 | 使用 using 声明时,就好像声明了相应的名称一样,如果某个名称已经在函数中声明了,则不能使用 using 声明导入相同的名称。而使用 using 编译指令时,将进行名称解析。如果函数中已经存在某个声明的名称,那么局部名称将会隐藏名称空间名,就像是隐藏同名的全局变量一样。 6 | 7 | 这样的文字说明看起来有些拗口,我们来看一个例子: 8 | 9 | ```C++ 10 | namespace Jill { 11 | double bukect(double n) {...} 12 | double fetch; 13 | struct Hill {...}; 14 | } 15 | 16 | char fetch; 17 | 18 | int main() { 19 | using namespace Jill; 20 | Hill thrill; 21 | double water = bucket(2); 22 | double fetch; 23 | cin >> fetch; // main函数中的fetch 24 | cin >> ::fetch; // 全局fetch 25 | cin >> Jill:fetch; // Jill名称空间中的fetch 26 | } 27 | 28 | void test() { 29 | Hill top; // 非法 30 | } 31 | ``` 32 | 33 | 在这段代码当中,名称`Jill::fetch`被放在局部名称空间中,因此它的作用域不是全局的,因此不会覆盖全局的`fetch`变量。然而局部声明的`fetch`将隐藏`Jill::fetch`和全局的`fetch`。 34 | 35 | 所以为了进行区分,需要我们在`fetch`之前加上作用于解析运算符。 36 | 37 | 还有一点需要注意,虽然我们在`main`函数中使用了 using 编译指令,它会将名称空间的名称视为是在函数之外声明的。但它不会使文件中的其他函数也能够使用这些名称,因此我们在`test`函数当中声明`Hill`是非法的。 38 | 39 | 一般来说,使用 using 声明比使用 using 编译指令更安全。因为 using 声明一次只会导入一个名称,如果和局部的名称发生冲突,那么编译器将会检查出冲突并提示。而 using 编译指令会导入名称空间中所有名称,包括可能并不需要的名称。如果发生了冲突,则会被局部名称覆盖,编译器也不会发出警告。 40 | 41 | 另外名称空间的名称可能分散在多个地方,也会增加烦恼,导致不知道添加了哪些名称。 42 | 43 | C++ Primer 中的实例偏好将`using namespace std`语句放在`main`函数当中,也有许多人喜欢将它放在全局名称空间中: 44 | 45 | ```C++ 46 | #include 47 | using namespace std; 48 | int main() { 49 | return 0; 50 | } 51 | ``` 52 | 53 | 这样做主要是为了方便,但最好不要这样做,还是使用名称空间解析运算符更加妥当: 54 | 55 | ```C++ 56 | int x; 57 | std::cin >> x; 58 | ``` 59 | -------------------------------------------------------------------------------- /C++/54-名称空间其他特性.md: -------------------------------------------------------------------------------- 1 | ## 名称空间其他特性 2 | 3 | ### 嵌套 4 | 5 | 我们可以将名称空间声明进行嵌套: 6 | 7 | ```C++ 8 | namespace elements { 9 | namespace fire { 10 | int flame; 11 | ... 12 | } 13 | float water; 14 | } 15 | ``` 16 | 17 | 我们观察一下可以发现这里的`flame`位于`elements::fire`当中,所以当我们使用解析运算符使用它的时候写成:`elements::fire::flame`。 18 | 19 | 同样,我们也可以使用 using 编译指令引入`fire`名称空间: 20 | 21 | ```C++ 22 | using namespace elements::fire; 23 | ``` 24 | 25 | ### 传递 26 | 27 | 并且我们还可以把 using 编译指令和 using 声明应用在名称空间当中: 28 | 29 | ```C++ 30 | namespace myth { 31 | using Jill::fetch; 32 | using namespace elements; 33 | using std::cout; 34 | using std::cin; 35 | } 36 | ``` 37 | 38 | 假设我们要访问`Jill::fetch`,由于它已经被引入`myth`当中了,所以我们可以这样访问: 39 | 40 | ```C++ 41 | std::cin >> myth::fetch; 42 | ``` 43 | 44 | 并且 using 编译指令是可以传递的,A 引入了 B,B 引入了 C,等价于 A 也引入了 C。比如当我们运行`using namespace myth;`,由于`myth`当中引入了`elements`,等价于同时运行了这两条: 45 | 46 | ```C++ 47 | using namespace myth; 48 | using namespace elements; 49 | ``` 50 | 51 | ### 别名 52 | 53 | 我们还可以给名称空间创建别名,例如: 54 | 55 | ```C++ 56 | namespace my = myth::elements::fire; 57 | using my::flame; 58 | ``` 59 | 60 | 在上面这个例子当中,我们将别名和名称空间的嵌套结合在了一起使用。其实有一点像是将名称空间当成是一种特殊变量处理的感觉。 61 | 62 | ### 匿名名称空间 63 | 64 | 我们还可以省略名称空间的名称来创建匿名名称空间。 65 | 66 | ```C++ 67 | namespace { 68 | int ice; 69 | int bandy; 70 | } 71 | ``` 72 | 73 | 它相当于后面跟着 using 编译指令一样,也就是说匿名的名称空间的潜在作用域为从声明点到该声明区域末尾。从这点来看,它与全局变量相似。但由于它是匿名的,所以无法在其他文件当中使用 using 关键字引入,所以不能在其他文件使用,某种意义上有些类似于链接性为内部的静态变量。 74 | -------------------------------------------------------------------------------- /C++/55-初探面向对象.md: -------------------------------------------------------------------------------- 1 | ## 初探面向对象 2 | 3 | 面向对象是我们编程当中必学的一课,也是软件工程当中非常非常重要的一环。 4 | 5 | 当我们初学编程的时候,完成的往往都是面向过程的程序。比如说计算圆的面积,或者是做一道 LeetCode 算法题。我们编写的所有代码完成的都是单纯的计算步骤,而面向对象是一次对计算步骤的抽象,它并不是一种技术,而是一种思维模式和编程理念。 6 | 7 | 我们以微信当年的打飞机游戏为例。 8 | 9 | ![](https://tva1.sinaimg.cn/large/008i3skNgy1gx3uchj9k8j30fa0dkmxd.jpg) 10 | 11 | 假设我们要来开发这样一个游戏,如果我们以面向过程的思路去编程。那么我们需要用许多变量来表示敌机、己方飞机等等。对于一架飞机来说,它会有许多属性,比如说血量、弹药、位置等等。那么就意味着我们需要使用许多数组来分别存储这些变量。 12 | 13 | 显然,这种方式的代码编写难度很大,因为变量很多,逻辑也很乱,很容易写着写着就晕了。 14 | 15 | 所以我们可以对问题进行一次抽象,可以简单理解成,将一些变量聚合成一个实体,针对每一个实体开发功能。比如说我们可以把飞机看成是一个实体,在这个实体当中赋予属性。比如说血量、位置、攻击力等等。在面向对象当中我们把这样的实体叫做类,其实可以简单理解成是一种特殊的结构体。 16 | 17 | ```C++ 18 | class Plane { 19 | int x, y; // 位置 20 | int health; // 血量 21 | int attack; // 攻击力 22 | bool alive; // 是否存活 23 | }; 24 | ``` 25 | 26 | 这就是一个最简单的类,除了把`struct`换成了`class`,几乎和结构体是一样的。按照同样的方法, 我们可以定义出其他的一些类,甚至整个游戏显示的界面也可以抽象成一个类。 27 | 28 | 到目前为止,看起来我们只是定义了一个结构体。实际上不止如此,我们可以给每一个类实现一些函数,丰富它的功能。 29 | 30 | 比如说,飞机受到攻击就会扣血,我们可以添加一个受到攻击之后的处理函数,再比如飞机移动坐标会发生变化。再比如飞机还可以发动攻击,朝着某个方向开炮…… 31 | 32 | 所以这个类会变成这样: 33 | 34 | ```C++ 35 | class Plane { 36 | int x, y; // 位置 37 | int health; // 血量 38 | int attack; // 攻击力 39 | bool alive; // 是否存活 40 | void move() { 41 | // todo 42 | } 43 | void beAttack(int att) { 44 | // todo 45 | } 46 | void attack() { 47 | // todo 48 | } 49 | }; 50 | ``` 51 | 52 | 也就是说我们围绕这个实体,把所有相关的功能都聚合到了一起。当我们把功能当中所有的实体都抽象完成之后,那么一系列复杂的运算逻辑,就可以抽象成实体之间的一系列交互。这样不仅代码更加简洁,可读性也更高。 53 | 54 | 如果是初次接触面向对象的话,会觉得有些困惑是正常的。但这毕竟不是一个很复杂的思想,随着代码量的增加,很快就能熟悉起来,也就不会觉得困惑了。 55 | -------------------------------------------------------------------------------- /C++/56-类的定义.md: -------------------------------------------------------------------------------- 1 | ## 类的定义 2 | 3 | 根据 C++ Primer 中的描述,类的定义是一种将抽象转换为用户定义类型的 C++工具。也就是说类的实质是一种用户自定义类型,它可以将数目表示和操作数据的方法组合成一个整洁的包。 4 | 5 | 在实际开发当中,想要实现一个类,并编写一个使用它的程序是相对比较复杂的,涉及多个步骤。 6 | 7 | 通常,我们会将类的定义放在头文件当中,并将实现的代码放在源代码文件中。我们来看 C++ Primer 当中的一个例子:一个关于股票买卖的类。 8 | 9 | 首先是类的定义,写在`stock00.h`文件中。 10 | 11 | ```C++ 12 | #ifndef STOCK00_H_ 13 | #define STOCK00_H_ 14 | 15 | #include 16 | 17 | class Stock { 18 | private: 19 | std::string company; 20 | long shares; 21 | double share_val; 22 | double total_val; 23 | void set_tot() {total_val = shares * share_val;} 24 | public: 25 | void accquire(const std::string &co, long n, double pr); 26 | void buy(long num, double price); 27 | void sell(long num, double price); 28 | void update(double price); 29 | void show(); 30 | }; 31 | #endif 32 | ``` 33 | 34 | 首先是关键字`class`,它表示我们声明的是一个类,一般类名我们使用大驼峰命名法定义。 35 | 36 | 其次在这个类的定义当中,我们看到了两个新出现的关键字`private`和`public`。这两个关键字描述了对类成员的访问控制,使用类对象的程序,都可以直接访问公有部分(public),但无法访问对象的私有成员。 37 | 38 | 想要获取私有成员,只能通过公有的成员函数。比如在上面这个例子当中,我们想要修改`share_val`只能通过公有的方法`sell`或`buy`,而无法直接对变量的值进行修改。防止程序直接访问数据修改数据被称为数据隐藏。 39 | 40 | 除了`public`和`private`,C++当中还提供了第三个关键字叫做`protected`,这个关键字和类的继承有关,我们将会在之后的文章当中进行讨论。 41 | 42 | 数据隐藏的思想其实是认为类中的成员变量的所有权应该属于类本身,当我们需要对类中的数据进行读取和修改的时候,应当通过类提供的公开方法,而不是直接操作类中值。这被认为是一种面向对象的思想,即只能通过类提供的方法访问数据,而不应该直接访问数据。 43 | 44 | C++正是基于这种面向对象的思想设计的,所以类中的成员默认是`private`的,我们可以省去`private`关键字: 45 | 46 | ```C++ 47 | class World { 48 | float mass; 49 | char name[30]; 50 | 51 | public: 52 | void tellall(); 53 | ... 54 | } 55 | ``` 56 | 57 | 我们从类的描述看上去很像是包含了成员函数以及数据隐藏的结构体,但实际上 C++对结构体进行了拓展,让它具有了和类相同的特性。比如我们也可以给结构体设计构造函数,也可以给结构体添加成员函数,甚至结构体也可以继承。 58 | 59 | 它们之间唯一的区别是,结构体的默认类型是`public`,而类是`private`。所以通常使用类来实现对象,而结构体只用来结构化地存储数据。 60 | 61 | 另外多说一句,数据隐藏的面向对象理念并不适用于所有语言。比如 Python,Python 当中的类中的成员变量默认是`public`,且没有`private`关键字。 62 | -------------------------------------------------------------------------------- /C++/57-类的实现.md: -------------------------------------------------------------------------------- 1 | ## 类的实现 2 | 3 | 当我们完成了类定义之后, 还需要来实现类当中的函数。 4 | 5 | 比如我们在`stock00.h`当中定义了一个类: 6 | 7 | ```C++ 8 | #ifndef STOCK00_H_ 9 | #define STOCK00_H_ 10 | 11 | #include 12 | 13 | class Stock { 14 | private: 15 | std::string company; 16 | long shares; 17 | double share_val; 18 | double total_val; 19 | void set_tot() {total_val = shares * share_val;} 20 | public: 21 | void accquire(const std::string &co, long n, double pr); 22 | void buy(long num, double price); 23 | void sell(long num, double price); 24 | void update(double price); 25 | void show(); 26 | }; 27 | #endif 28 | ``` 29 | 30 | ### 成员函数 31 | 32 | 在这个定义当中,我们只是声明了函数,并没有具体实现函数的逻辑。 33 | 34 | 我们通常会在同名的 cpp 文件当中实现,实现的时候,需要使用作用域解析运算符来表示函数所属的类: 35 | 36 | ```C++ 37 | void Stock::update(double price) { 38 | ... 39 | } 40 | ``` 41 | 42 | 这样就表明了`update`函数所属`Stock`这个类,这也就意味着我们可以创建属于其他类的同名函数: 43 | 44 | ```C++ 45 | void Buffoon::update() { 46 | ... 47 | } 48 | ``` 49 | 50 | 其次,我们在成员函数当中,可以访问`private`限定的成员变量。比如说在`show`函数当中,我们可以这样实现: 51 | 52 | ```C++ 53 | void Stock::show() { 54 | std::cout << company << shares << share_val << total_val << endl; 55 | } 56 | ``` 57 | 58 | 虽然`company`,`shares`都是类的私有成员,但在成员方法当中,一样可以正常访问。 59 | 60 | 再次,我们在成员方法当中调用另外一个成员方法,可以不需要解析运算符。比如我们要在`show`函数内调用`update`函数,直接使用`update()`即可,而无需前面的`Stock::`。 61 | 62 | ### 内联函数 63 | 64 | 我们再回过头来看一下`Stock`这个类的定义,在类的定义当中,有一个叫做`set_tot`的函数。我们直接在类当中实现了逻辑。虽然同样是成员函数,但是在类当中直接实现的函数是有所区别的。在类声明当中实现的函数,会被视为是内联函数。 65 | 66 | 一般我们会把一些比较简短的函数在类的声明当中直接实现,当然我们也可以使用关键字`inline`,手动指定某个函数是内联的。 67 | 68 | ```C++ 69 | class Stock { 70 | private: 71 | void set_tot(); 72 | public: 73 | ... 74 | }; 75 | 76 | inline void Stock::set_tot() { 77 | total_val = shares * share_val; 78 | } 79 | ``` 80 | -------------------------------------------------------------------------------- /C++/58-构造函数.md: -------------------------------------------------------------------------------- 1 | ## 构造函数 2 | 3 | 我们定义了类之后,在使用之前,往往还需要对类进行初始化。这篇介绍的就是对类进行初始化的方法。 4 | 5 | 像是结构体,我们可以使用列表初始化的方法进行初始化: 6 | 7 | ```C++ 8 | struct Thing { 9 | char *pn; 10 | int m; 11 | }; 12 | 13 | Thing th = {"hello", 23}; 14 | ``` 15 | 16 | 但类不行,因为结构体当中的成员变量都是`public`的,而类往往是私有的。这意味着我们不能直接用程序访问数据成员,需要设计成函数。 17 | 18 | 在 C++当中,这种用来构造类的函数,被称为类构造函数。构造函数的原型和函数头有一个比较明显的特征——它虽然没有返回值,但没有被声明称 void 类型,实际上构造函数没有声明类型。 19 | 20 | 比如我们还是之前的类: 21 | 22 | ```C++ 23 | class Stock { 24 | private: 25 | std::string company; 26 | long shares; 27 | double share_val; 28 | double total_val; 29 | void set_tot() {total_val = shares * share_val;} 30 | public: 31 | void accquire(const std::string &co, long n, double pr); 32 | void buy(long num, double price); 33 | void sell(long num, double price); 34 | void update(double price); 35 | void show(); 36 | }; 37 | #endif 38 | ``` 39 | 40 | 现在我们要加入构造函数,首先是在类中加上声明。 41 | 42 | ```C++ 43 | class Stock { 44 | ... 45 | Stock(const string &co, long n=0, double pr=0.0); 46 | } 47 | ``` 48 | 49 | 注意看,我们实现定义的时候,函数是没有返回类型的: 50 | 51 | ```C++ 52 | Stock::Stock(const string &co, long n, double pr) { 53 | company = co; 54 | if (n < 0) { 55 | std::cerr << "Number of shares can't be negative;" 56 | << company << " shares set to 0.\n"; 57 | shares = 0; 58 | }else { 59 | shares = n; 60 | share_val = pr; 61 | set_tot(); 62 | } 63 | } 64 | ``` 65 | 66 | 注意,构造函数中的参数名不能和类成员名一致,否则会引起错误: 67 | 68 | ```C++ 69 | Stock::Stock(const string &company, long shares, double share_val) {} 70 | ``` 71 | 72 | 如果一致的话,就会出现这样的代码: 73 | 74 | ```C++ 75 | shares = shares; 76 | ``` 77 | 78 | 为了避免这种混乱,一般会在代码风格层面加以区分。比如在谷歌代码规范当中,类中的`private`成员变量中需要使用后缀\_。 79 | 80 | 最后,我们来看下构造函数的使用。C++当中支持两种方式,我们先来看第一种,显式地调用: 81 | 82 | ```C++ 83 | Stock food = Stock("word", 250, 2.5); 84 | ``` 85 | 86 | 另外一种方式是隐式地调用: 87 | 88 | ```C++ 89 | Stock garment("furry", 50, 2.5); 90 | ``` 91 | 92 | 这种方式更加紧凑,我们每次使用 new 动态分配内存时,也会使用类构造函数。 93 | 94 | ```C++ 95 | Stock *pstock = new Stock("ele", 18, 19.0); 96 | ``` 97 | 98 | 构造函数的使用不同于一般的类方法,我们无法使用对象来调用构造函数。 99 | -------------------------------------------------------------------------------- /C++/59-默认构造函数.md: -------------------------------------------------------------------------------- 1 | ## 默认构造函数 2 | 3 | 上一篇文章当中介绍的是显式构造函数,也就是说我们需要传入值来对类的成员变量进行初始化。 4 | 5 | 但也有一些情况,我们可能并不需要传入值进行初始化,或者有一些其他的逻辑。针对这种情况,C++提供了默认构造函数的功能。所谓默认构造函数,也就是在没有提供显式的初始值时,用来创建对象的构造函数。 6 | 7 | 默认构造函数非常简单,还是以之前的`Stock`类为例: 8 | 9 | ```C++ 10 | Stock::Stock() { 11 | ... 12 | } 13 | ``` 14 | 15 | 如果默认构造函数里没有任何逻辑,我们也可以把它写在类声明里: 16 | 17 | ```C++ 18 | class Stock { 19 | ... 20 | Stock() {} 21 | }; 22 | #endif 23 | ``` 24 | 25 | C++当中有一个很奇怪的设定,当一个类我们没有提供任何构造函数时,C++会默认赋予它一个默认构造函数。但当我们一旦实现了构造函数之后,C++则不会进行如此操作。意味着对于定义了构造函数的类来说,想要使用默认构造函数,必须由程序员手动定义。 26 | 27 | 有了默认构造函数之后,我们才可以直接声明类的实例: 28 | 29 | ```C++ 30 | Stock stock1; 31 | ``` 32 | 33 | 如果`Stock`类没有默认构造函数,那么上述的语句会报错。 34 | 35 | 实现默认构造函数,除了上述那样手动创建一个没有任何参数的构造函数之外,还可以将已有的构造函数的所有参数都设置默认值。 36 | 37 | ```C++ 38 | Stock(const string &co = "Error", int n = 0, double pr = 0.0); 39 | ``` 40 | 41 | 由于只能有一个默认构造函数,所以不要同时采用这两种方式。 42 | -------------------------------------------------------------------------------- /C++/6-char类型与io加速.md: -------------------------------------------------------------------------------- 1 | ### char 类型 2 | 3 | char 的全称是 character,也就是字符的意思。顾名思义,char 类型是专门为了存储字符而设计的。 4 | 5 | 计算机存储数字非常方便,只需要将其转化成二进制即可。但存储字符就有点麻烦了,一般都是通过对字符进行数字化编码。这也就是为什么 char 类型本质上是另外一种整数,因为它存储的其实是字符的数字编码。 6 | 7 | char 一共有 8 个二进制位,即一个字节,理论上能够存储 256 个字符。基本上足够涵盖计算机当中所有的字母、标点符号以及数字,即 ASCII 码。 8 | 9 | ASCII 的全称是美国信息交换标准代码,它是一套电脑编码系统,包含了所有英文字母以及标点符号和一些特殊字符。全表一共有 128 个字符,刚好可以用一个 char(有符号)来存储。 10 | 11 | 大家可以参考一下下表,Dec 表示编号,Char 表示字符。 12 | 13 | ![](https://tva1.sinaimg.cn/large/008i3skNgy1gvi1hcb1d5j60j40ey40g02.jpg) 14 | 15 | 其中数字 0 的编号是 48,字母 a 的编号是 97,大写字母 A 的标号是 65。 16 | 17 | 当我们把一个字符赋值给 char 型变量的时候,它会去查 ASCII 表,找到字符对应的编号。同样,当我们使用`%c`输出一个字符的时候,它也会去寻找 char 中存储的编码对应的符号进行输出。 18 | 19 | 既然字符在 C++当中都是以数字的形式存储的,那么我们就可以对它来进行加减运算。 20 | 21 | 比如: 22 | 23 | ```C++ 24 | char c = 'a'; 25 | cout << ++c << endl; 26 | ``` 27 | 28 | 得到的结果是'b',有加自然也有减,我们也可以对它做减法操作。 29 | 30 | ```C++ 31 | char c = 'b'; 32 | cout << --b << endl; 33 | ``` 34 | 35 | 得到的结果就是'a'。 36 | 37 | 另外,我们还可以对于两个 char 类型的变量进行减法操作。比如用得比较多的就是将字符型的数字转成 int 型。 38 | 39 | ```C++ 40 | char c = '1'; 41 | int num = c - '0'; 42 | ``` 43 | 44 | 这样我们得到的 num 就是数字型的 1。 45 | 46 | 再比如,我们还可以通过大于小于符号来判断 char 类型的范围: 47 | 48 | ```C++ 49 | char c = '1'; 50 | if (c >= '0' && c <= '9') { 51 | cout << "c is a number" << endl; 52 | } 53 | ``` 54 | 55 | ### getchar、putchar、cin.get、cout.put 56 | 57 | `getchar`和`putchar`都是 C 语言当中专门面向字符 IO 的函数,也就是读入和输出字符的函数。 58 | 59 | 因为确定了处理的数据类型是字符,不需要额外的格式说明,因此`getchar`和`putchar`的效率要比`scanf`和`printf`更高。 60 | 61 | 所以在算法竞赛领域,有人为了提升程序的性能,丧心病狂地使用`getchar`代替`scanf`来读入数据。 62 | 63 | 我这里贴一段使用`getchar`来读入`int`型的代码,给大家做一个参考。这个属于标准的奇淫技巧,不推荐使用。 64 | 65 | ```C++ 66 | void read(int &x) { 67 | int f = 1; x = 0; char s = getchar(); 68 | while (s < '0' || s > '9') { 69 | if (s == '-') { 70 | f = -1; 71 | s = getchar(); 72 | } 73 | } 74 | while (s >= '0' && s <= '9') { 75 | x = x * 10 + s - '0'; 76 | s = getchar(); 77 | } 78 | x *= f; 79 | } 80 | ``` 81 | 82 | `cin.get`和`cout.put`与`getchar`和`putchar`的用法类似,只不过是 C++当中的特性。大家可以参考一下下面这个例子,就不过多赘述了。 83 | 84 | ```C++ 85 | char c; 86 | cin.get(c); 87 | cout.put(c); 88 | ``` 89 | 90 | ### 输入输出中文 91 | 92 | 关于这一段我犹豫了很久要不要加,因为实在是没有相关经验,毕竟之前只刷题了。纠结了很久还是决定写上,因为这个问题对于不少同学应该挺重要的,尤其是想要做 C++工程的同学。本人水平有限,勉强整理了一下各方资料,如有错误,欢迎指出~ 93 | 94 | 其实直接在 C++当中是可以直接输出中文的,这并不会有什么问题。 95 | 96 | 比如下列代码,是可以完美运行的: 97 | 98 | ```C++ 99 | string str; 100 | cin >> str; 101 | cout << str << endl; 102 | cout << str.length() << endl; 103 | ``` 104 | 105 | ![](https://tva1.sinaimg.cn/large/008i3skNgy1gvi3h4jiw5j60uc04y0sw02.jpg) 106 | 107 | 只是为什么最后输出的长度是 6?因为我是在 Mac 上跑的这段代码。在 Mac 当中默认使用 utf-8 编码,一个汉字的长度是 3 个字节。C++当中的字符串计算长度的时候统计的是字节的数量,所以两个汉字的长度是 6。 108 | 109 | 如果我们是在源代码当中写入了中文,比如: 110 | 111 | ```C++ 112 | string str = "中文"; 113 | cout << str << endl; 114 | ``` 115 | 116 | 这就可能一些问题,最常见的问题就是代码存储环境和运行环境的默认编码不同,比如 IDE 当中默认是 utf-8 编码,但是终端默认是 gbk 编码(windows 系统常见)。这就会导致输出的结果是乱码。 117 | 118 | 解决方案是我们可以使用`wchar_t`,`wchar_t`即`char`的宽类型版本,它占据两个字节。可以用来存储 unicode 编码的字符: 119 | 120 | ```C++ 121 | const wchar_t* str = L"中文"; 122 | ``` 123 | 124 | 我们在中文两个字之前加上了 L 修饰符,它告诉编译器,这是一个宽字符,我们需要编译器根据 locale 来进行翻译。 125 | 126 | `locale`是指根据计算机用户使用的语言、所在的国家或地区以及文化传统而定义的软件运行时的语言环境。可以将`locale`理解为一系列环境变量。`locale`环境变量值的格式为`language_area.charset`。languag 表示语言,例如英语或中文;area 表示使用该语言的地区,例如美国或者中国大陆;charset 表示字符集编码,例如 UTF-8 或者 GBK。 127 | 128 | 这些环境变量会对日期格式,数字格式,货币格式,字符处理等多个方面产生影响。在`Linux`系统下打开`Terminal`,输入`locale`命令,就可查看当前系统使用的语言环境。 129 | 130 | ![](https://tva1.sinaimg.cn/large/008i3skNgy1gvi4r2ag64j613g0ao0tk02.jpg) 131 | 132 | `locale`的结果包含 12 类,我在网上也找到了表格: 133 | 134 | ![](https://tva1.sinaimg.cn/large/008i3skNgy1gvi4sm7j7zj61is0twjuu02.jpg) 135 | 136 | `LANG`指的是未设置的默认值,大部分程序应用`LANGUAGE`指定的语言作为界面语言。`LC_ALL`同时设置所有的内容,并且其优先级比每个内容单独设置的优先级都高,而`LANG`的优先级最低。 137 | 138 | `cin`和`cout`可以看成是针对 char 的流,所以不适合应用在`wchar_t`类型的处理上。与之对应我们应该使用`wcin`和`wcout`。而`wcout`默认采用的是 C local,并不认识中文,所以我们要先对`wcout`的 local 进行设置。将其设置成和运行环境的 local 一致。 139 | 140 | 大约有以下几种设置方法: 141 | 142 | ```C++ 143 | #include 144 | const wchar_t* str = L"中文"; 145 | 146 | // 使用默认local 147 | locale loc(""); 148 | wcout.imbue(loc); 149 | 150 | // 使用local命令显示的结果 151 | locale loc("en_US.UTF-8"); 152 | wcout.imbue(loc); 153 | 154 | // 使用标准facet 155 | locale utf8(locale(), new codecvt_utf8_utf16 ); 156 | wcout.imbue(utf8); 157 | 158 | // 使用系统local 159 | locale sys_loc(""); 160 | wcout.imbue(sys_loc); 161 | 162 | wcout << str << endl; 163 | cout << wcslen(str) << endl; 164 | ``` 165 | 166 | 我们可以使用 wcslen 来计算宽字节字符串的长度,它输出的结果是 2,而不是 6。 167 | 168 | C++当中的编码设置是一个很大的问题,因为在刷题当中几乎不会遇到,我们这里也只是做一个浅尝辄止的讨论。大家如果有需要,可自行深入研究。 169 | 170 | > 参考资料: 171 | > 172 | > - [C 语言的国际化](https://blog.csdn.net/lemonrabbit1987/article/details/48152601) 173 | > - C++ Primer(第六版) 174 | -------------------------------------------------------------------------------- /C++/60-析构函数.md: -------------------------------------------------------------------------------- 1 | ## 析构函数 2 | 3 | 当我们使用构造函数创建对象之后,程序负责跟踪对象,直到对象过期位置。 4 | 5 | 对象过期时,程序会自动调用一个特殊的成员函数,这个成员函数就叫做析构函数。析构函数这个翻译有一些隐晦,它的英文是 deconstructor,我个人感觉翻译成销毁函数更确切一些。 6 | 7 | 也就是说当对象不再使用,即将被销毁的时候会调用析构函数。如果我们构造函数当中创建的都是存储持续性的变量,也就是不是使用`new`创建的对象。这些对象会自动销毁,并不需要析构函数执行什么逻辑。如果构造函数当中使用了`new`动态分配了内存,那么需要在析构函数当中添加对应的`delete`语句,将内存释放。 8 | 9 | 比如这个例子: 10 | 11 | ```C++ 12 | class Algo { 13 | public: 14 | Algo(const char* name) { 15 | name_ = new char[strlen(name)+1]; 16 | strcpy(name_, name); 17 | } 18 | private: 19 | char *name_; 20 | } 21 | ``` 22 | 23 | 在这个例子当中,`Algo`类当中的`name_`变量是通过`new`动态分配的,那么当`Algo`的实例销毁的时候,需要我们在析构函数当中手动执行`delete`的逻辑。 24 | 25 | 析构函数和构造函数几乎完全一样,只在类名前加上`~`。析构函数也可以没有返回值和声明类型,并且析构函数没有参数。 26 | 27 | 加上析构函数之后,上面的例子是这样的: 28 | 29 | ```C++ 30 | class Algo { 31 | public: 32 | Algo(const char* name) { 33 | name_ = new char[strlen(name)+1]; 34 | strcpy(name_, name); 35 | } 36 | ~Algo() { 37 | delete []name_; 38 | } 39 | private: 40 | char *name_; 41 | } 42 | ``` 43 | 44 | 有一点需要注意,析构函数调用应该是由编译器决定,如果创建的是静态存储类对象,则析构函数在程序结束时自动调用,如果创建的是自动存储类对象,析构函数会在程序执行完代码块时被自动调用。如果是通过 new 创建的,那么则在使用`delete`时被调用。一般我们不会手动调用析构函数。 45 | 46 | 由于类对象过期时析构函数会被自动调用,因此必须有一个析构函数。如果程序员没有提供析构函数,那么编译器将隐式地声明一个默认析构函数。 47 | -------------------------------------------------------------------------------- /C++/61-this指针.md: -------------------------------------------------------------------------------- 1 | ## this 指针 2 | 3 | 还是`Stock`股票这个类,假设我们要实现一个方法,比较一下当前股票和传入的股票, 返回价格高的那个股票。在我们实现的时候,会遇到一点问题。 4 | 5 | ```C++ 6 | const Stock & Stock::topVal(const Stock &s) const { 7 | if (s.total_val > total_val) { 8 | return s; 9 | }else { 10 | return ????; 11 | } 12 | } 13 | ``` 14 | 15 | 这段代码当中有一些问题,我们一个一个来说。 16 | 17 | 首先说函数签名,前面都没有问题,问题出现在签名末尾的`const`。这是我们第一次在函数的末尾看到`const`,这个用法只能用在类或结构体的成员函数中,而不能用在普通函数里。 18 | 19 | 它表示这个函数不会修改任何成员变量的值,末尾加上`const`的函数称为常量成员函数。 20 | 21 | 常量函数有一些特殊的性质: 22 | 23 | - 能被任何函数调用,只能调用常量函数 24 | - 能被任何对象调用,但`const`对象只能调用`const`函数 25 | 26 | 性质不是非常复杂,但有些像是绕口令,需要从`const`常量的性质角度出发理解一下。 27 | 28 | 代码当中第二个问题就是一堆问号的地方,这里我们想要`return`当前对象,但是我们不知道如何表示。为了解决这个问题,C++当中创建了一个特殊的指针叫做`this`,它用来指向调用成员函数的对象,也就是当前对象。 29 | 30 | 所以有了`this`之后,这段代码可以写成: 31 | 32 | ```C++ 33 | const Stock & Stock::topVal(const Stock &s) const { 34 | if (s.total_val > total_val) { 35 | return s; 36 | }else { 37 | return *this 38 | } 39 | } 40 | ``` 41 | 42 | 另外,`this`指针也可以拿来访问当前对象当中的成员变量,比如我们想要访问当前的`total_val`,可以写成`this->total_val`,我们直接用`total_val`本质上是一样的,是编译器替我们做了优化。 43 | -------------------------------------------------------------------------------- /C++/62-类枚举.md: -------------------------------------------------------------------------------- 1 | ## 类常量 2 | 3 | 有的时候, 我们希望能给类当中定义一些常量,可以给所有类的对象使用。 4 | 5 | 比如说我们在类当中定义一个数组,希望可以定义一个常量,用来初始化数组的长度。既然是用来初始化数组长度的,那么这个值自然也不会改变,我们定义成`const`是否可行呢?比如这样: 6 | 7 | ```C++ 8 | class P { 9 | private: 10 | const int N=15; 11 | int costs[N]; 12 | ... 13 | } 14 | ``` 15 | 16 | 很遗憾,这样不行,编译器会抛出一个错误: 17 | 18 | ![](https://tva1.sinaimg.cn/large/008i3skNgy1gxbx0p5vktj311u07ymxw.jpg) 19 | 20 | 说我们`non-static`变量 N 用的不对,看到了吧?报错信息里的描述是`non-static`变量,也就是说我们这里定义的 N 不是一个 static 类型的,所以不能用来初始化数组。 21 | 22 | 那怎么样才算对了呢?很简单,我们可以定义成 static 变量就对了。 23 | 24 | ```C++ 25 | class P { 26 | private: 27 | static const int N=15; 28 | int costs[N]; 29 | ... 30 | } 31 | ``` 32 | 33 | 也就是在`const int`前面加上了 static 关键字,表示这是一个 static 即静态变量。这个常量会和其他静态变量存储在一起,而不是存储在对象里,这样的话也就被所有对象共享了。 34 | 35 | 对于像是 Java、Python 等其他语言来说,类中的静态变量是可以通过类名直接访问的,而 C++中不行。 36 | 37 | 除了定义静态变量之外,还有一种方法就是定义类枚举: 38 | 39 | ```C++ 40 | class P { 41 | private: 42 | enum {N=15}; 43 | int costs[N]; 44 | ... 45 | } 46 | ``` 47 | 48 | 这样也能运行,看起来非常不可思议。 49 | 50 | 通过这种方式声明并不会创建类数据成员,对于类的对象来说,并不包含枚举,这里的 N 只是一个符号名称。在类当中遇到它的时候,编译器会用 15 来代替。 51 | 52 | 也正是因为我们只是为了创建符号常量,而不是创建枚举类型的变量,所以不需要提供枚举名。有一些 C++的类库当中也用到了这种方法,比如`ios_base::fixed`等。 53 | 54 | ## 类枚举 55 | 56 | 传统的枚举类型变量存在一定的问题,最大的问题是当两个枚举定义中的枚举量重名的时候,就会发生冲突: 57 | 58 | ```C++ 59 | enum A {small, big, medium}; 60 | enum B {small, large, xlarge}; 61 | ``` 62 | 63 | 这两个枚举类型当中都有`small`,如果位于相同的作用于,那么就会发生冲突。 64 | 65 | 为了避免这个问题,C++11 当中提供了一种新枚举,它的作用域为类,声明类似这样: 66 | 67 | ```C++ 68 | enum class A {small, big, medium}; 69 | enum class B {small, large, xlarge}; 70 | ``` 71 | 72 | 和上面的代码几乎是一样的,只不过多了一个关键字`class`,换成`struct`也是可以的。 73 | 74 | 当我们使用的时候,需要加上解析运算符: 75 | 76 | ```C++ 77 | A choice = A::small; 78 | B c = B::large; 79 | ``` 80 | 81 | 另外我们前面说过,常规的枚举将自动转化为整型,比如赋值给 int 变量或者是用于比较表达式的时候。而作用域内枚举不会隐式地转换类型。 82 | -------------------------------------------------------------------------------- /C++/63-抽象数据类型.md: -------------------------------------------------------------------------------- 1 | ## 抽象数据类型 2 | 3 | 我们在学数据结构的时候,经常遇到的一个概念就是抽象数据类型(Abstract Data Type),简称 ADT。 4 | 5 | 维基百科中的定义是:抽象数据类型是计算机科学中具有类似行为的特定类别的数据结构的数学模型,或者具有类似语义的一种或多种[程序设计语言](https://zh.wikipedia.org/wiki/程序设计语言)的[数据类型](https://zh.wikipedia.org/wiki/数据类型)。 6 | 7 | 从这段定义来看,非常地费解,其实我们只需要抓住核心。核心就是接口和实现的分离。我们在使用一个 ADT 的时候,只需要和接口进行交互,而不必关心接口中的实现细节。同样,数据也是隐藏不可见的,也需要通过接口进行交互。 8 | 9 | 也就是说接口是数据类型唯一的交互方式,除此之外,用户无法接触到 ADT 的数据以及实现细节。 10 | 11 | 举个例子,以栈举例,如果我们不将栈设计成 ADT,那么用户在使用栈的时候,可能就需要自己创建一个数组来存储栈中的数据,通过调用一些方法来实现栈的功能。但这势必需要用户了解栈的原理,以及数据存储的细节。ADT 会做一个良好的封装,用户只需要了解每个接口的功能,调用对应的接口实现自己想要的逻辑即可。 12 | 13 | 我们来看一下 C++ Primer 当中实现的栈的例子。 14 | 15 | 首先,我们需要知道栈一共有哪些接口,大概有如下这么几个: 16 | 17 | - 创建空栈 18 | - 可添加数据到栈顶 19 | - 可从栈顶弹出数据 20 | - 可查看栈是否为空 21 | - 可查看栈是否已满 22 | 23 | 然后,我们遵守 C++中面向对象的设计思路,将它封装在一个类当中。首先我们来定义这个类: 24 | 25 | ```C++ 26 | #ifndef STACK__H_ 27 | #define STACK__H_ 28 | 29 | typedef unsigned long Item; 30 | 31 | class Stack { 32 | private: 33 | enum {MAX=10}; 34 | Item items[MAX]; 35 | int top; 36 | public: 37 | Stack(); 38 | bool isempty() const; 39 | bool isfull() const; 40 | bool push(const Item &item); 41 | bool pop(Item &item); 42 | }; 43 | #endif 44 | ``` 45 | 46 | 我们来看下这个定义,会发现,其中的数据都被设定成了`private`,也就是用户无法直接访问到数据。只能通过`public`的接口进行交互,也无须关心其中的实现细节,可以当做黑盒使用。 47 | 48 | 最后, 我们再来看下 C++ Primer 当中给出的实现: 49 | 50 | ```C++ 51 | #include "stack.h" 52 | 53 | Stack::Stack() { 54 | top = 0; 55 | } 56 | 57 | bool Stack::isempty() const { 58 | return top == 0; 59 | } 60 | 61 | bool Stack::isfull() const { 62 | return top == MAX; 63 | } 64 | 65 | bool Stack::push(const Item &item) { 66 | if (top < MAX) { 67 | items[top++] = item; 68 | return true; 69 | } 70 | return false; 71 | } 72 | 73 | bool Stack::pop(Item &item) { 74 | if (top > 0) { 75 | item = items[--top]; 76 | return true; 77 | } 78 | return false; 79 | } 80 | ``` 81 | -------------------------------------------------------------------------------- /C++/64-运算符重载.md: -------------------------------------------------------------------------------- 1 | ## 运算符重载 2 | 3 | C++当中除了函数可以重载之外,其实运算符也是可以重载的。我们之前已经接触过一些,可能大家没有意识到。 4 | 5 | 举个例子,乘号`*`,运用在指针上,就是取值的意思,而运用在算数当中,则是乘法的意思。同样一个符号,用在不同的地方,起到了不同的效果。这其实就是一种重载,C++根据操作数的数目和类型来决定要使用哪一种操作。 6 | 7 | 另外 C++允许将运算符重载扩展到用户自定义的类型,也就是结构体和类当中。比如,我们可以将重载加号,对两个对象相加。 8 | 9 | 其实这种用法也出现过,就是`string`类,我们将两个字符串相加时,得到的是两个字符串的拼接。 10 | 11 | 我们通过`operator`加上运算符来定义一个重载运算符,需要注意的是,我们只能重载目前 C++当中已有的运算符。比如`operator []`将重载`[]`运算符,`operator +`重载加法运算符等等。 12 | 13 | 下面我们就来看一个例子: 14 | 15 | ```C++ 16 | #ifndef MYTIME0__H_ 17 | #define MYTIME0__H_ 18 | 19 | class Time { 20 | private: 21 | int hours; 22 | int minutes; 23 | public: 24 | Time(); 25 | Time(int h, int m=0); 26 | void AddMin(int m); 27 | void AddHr(int h); 28 | void Reset(int h=0, int m=0); 29 | Time Sum(const Time &t) const; 30 | void Show() const; 31 | }; 32 | #endif 33 | ``` 34 | 35 | 我们创建了一个 Time 类用来表示时间,还有当中配套的一些方法。我们着重看一下`Sum`函数,这个函数接收的是一个 Time 对象的引用,而返回的是一个 Time 对象。 36 | 37 | 我们来看下这个函数的具体实现: 38 | 39 | ```C++ 40 | Time Time::Sum(const Time &t) const { 41 | Time sum; 42 | sum.minutes = minutes + t.minutes; 43 | sum.hours = hours + t.hours + sum.minutes / 60; 44 | sum.minutes %= 60; 45 | return sum; 46 | } 47 | ``` 48 | 49 | 这一段逻辑表示两个时间相加,需要注意一下进位即可。我们将传入的参数设置为引用是为了提高参数传递的效率,返回的结果不能设置成引用是因为`sum`对象是局部对象,函数结束时将会被删除,因此引用就指向了一个不存在的对象。 50 | 51 | 我们可以将这个函数改写成重载加法运算符: 52 | 53 | ```C++ 54 | Time Time::operator+(const Time &t) const { 55 | Time sum; 56 | sum.minutes = minutes + t.minutes; 57 | sum.hours = hours + t.hours + sum.minutes / 60; 58 | sum.minutes %= 60; 59 | return sum; 60 | } 61 | ``` 62 | 63 | 除了函数签名改了一下之外,逻辑和之前是一样的。 64 | 65 | 我们在调用的时候,除了可以用函数名调用之外,也可以使用加号进行调用: 66 | 67 | ```C++ 68 | Time a, b; 69 | a.opeator+(b); 70 | a + b; 71 | ``` 72 | 73 | 这两种都是可以的,并且也是等价的。 74 | -------------------------------------------------------------------------------- /C++/65-重载限制.md: -------------------------------------------------------------------------------- 1 | ## 重载限制 2 | 3 | 上一篇我们讲了在类和结构体当中重载运算符,关于运算符的重载并不是随心所欲的。C++给出了一些限制,从而保证了规范,以及程序运行的准确性。 4 | 5 | 下面我们就来一一来看下: 6 | 7 | ### 必须至少有一个操作数是用户定义的类型 8 | 9 | 这句话看不明白没有关系,我们只需要记住它的目的就好了。它的主要目的是为了防止用户为了标准类型重载运算符。比如将+重载成两个数的差,而不是和。 10 | 11 | 这种限制对创造性有一定的影响,没有那么灵活,但可以保证程序的正常运行。不会出现一些反人类的情况 12 | 13 | ### 不能违反运算符原来的规则 14 | 15 | 这一点很好理解,比如+号,它的运算就是计算两个数的和。需要有两个操作数,现在我们把它重载成一个操作数,就是违法的。 16 | 17 | ```C++ 18 | P p; 19 | +p; 20 | ``` 21 | 22 | 同样,我们也不能修改运算符的优先级,如果将加号运算符重载成两个类相加,那么新的运算符和原来的加号拥有一样的优先级。 23 | 24 | ### 不能创建新运算符 25 | 26 | 这一点之前已经说过了,比如不能定义`operator @`等这种原来没有的运算符。 27 | 28 | ### 禁止名单 29 | 30 | 有一些运算符是禁止重载的,如:`sizeof, ., ::, ?:, typeid, const_cast, dynamic_cast, reinterpret_cast, static_cast` 31 | 32 | 这些运算符往往都有特殊的功能,直接从实现层面禁止重载。 33 | 34 | ### 部分运算符只能通过成员函数重载 35 | 36 | 大多数运算符都可以通过成员函数以及非成员函数进行重载,但也有部分例外,只能通过成员函数重载,如: 37 | 38 | - =:赋值运算符 39 | - (): 函数调用运算符 40 | - []: 下标运算符 41 | - ->: 箭头符号 42 | 43 | 这里的非成员函数看起来有些费解,之后我们会遇到,主要是指友元函数。 44 | -------------------------------------------------------------------------------- /C++/66-友元函数.md: -------------------------------------------------------------------------------- 1 | ## 友元函数 2 | 3 | 我们知道 C++控制对象的私有部分的访问,只能通过公共的接口。这样的设计当然没错,但有的时候也会显得过于严格,产生一些问题。 4 | 5 | 因此 C++提供了另外一种形式的访问权限,叫做友元(friend)。 6 | 7 | 友元有三种,分别是友元函数、友元类和友元成员函数。 8 | 9 | 通过让函数成为类的友元,可以赋予该函数与类成员函数一样的访问权限,也就是说我们可以在友元函数当中访问类的私有成员变量。 10 | 11 | 在介绍友元函数的使用之前,我们需要先了解为什么需要友元函数。C++ Primer 中给了一个非常不错的例子,在之前运算符重载的例子当中,我们实现了一个类`Time`。用来记录时间,假设我们需要重载它的\*运算符,能够允许一个时间对象和一个浮点数相乘。 12 | 13 | 很明显,我们只需要重载运算符\*即可: 14 | 15 | ```C++ 16 | Time Time::operator*(const double x) { 17 | // todo 18 | } 19 | ``` 20 | 21 | 我们在使用的时候大概是这样: 22 | 23 | ```C++ 24 | Time a, b; 25 | a = b * 32.5; 26 | ``` 27 | 28 | 但是这里有一个小问题,我们写成`a = b * 32.5;`可以,但如果反过来写成`32.5 * b`就不行了。因为对于`b * 32.5`来说本质上是 b 调用了`operator*`函数,等价于`a = b.opeartor*(32.5);`。但后者就不行了,要怎么解决呢,只能另外实现一个函数来解决了,这个函数有两个 input,分别是`double`和`Time`类型,返回一个`Time`类型。 29 | 30 | ```C++ 31 | Time operator*(double m, const Time &t); 32 | ``` 33 | 34 | 但这又有了新的问题,由于这不是一个成员函数,不能直接访问类的私有数据。为了破例让它能够访问,我们需要将它设置成友元。 35 | 36 | 创建友元的方法很简单,我们只需要在函数签名之前加上关键字`friend`。 37 | 38 | ```C++ 39 | friend Time operator*(double m, const Time &t); 40 | ``` 41 | 42 | 它有两个含义: 43 | 44 | - 它不是成员函数,因此不能使用成员函数运算符来调用 45 | - 它与成员函数的访问权限相同,即可以访问所有 private 和 public 数据 46 | 47 | 由于友元函数不是成员函数,所有我们在实现的时候不需要使用`Time::`限定符,也不用在实现当中加上关键字`friend`,函数的实现如下: 48 | 49 | ```C++ 50 | Time operator*(double m, const Time &t) { 51 | Time result; 52 | long totalminutes = t.hours * m * 60 + t.minutes * m; 53 | result.hours = totalminutes / 60; 54 | result.minutes = totalminutes % 60; 55 | return result; 56 | } 57 | ``` 58 | 59 | 我们在函数当中直接访问了`hours`和`minutes`成员变量,因此函数必须是友元函数。 60 | 61 | 当然我们可以把函数稍微变换一下,就可以不必是友元函数了: 62 | 63 | ```C++ 64 | Time operator*(double m, const Time &t) { 65 | return t * m; // 调用了t.operator*(m) 66 | } 67 | ``` 68 | 69 | 在这个函数当中,我们没有显式地访问私有变量,因此可以不必是友元。 70 | -------------------------------------------------------------------------------- /C++/67-重载<<运算符.md: -------------------------------------------------------------------------------- 1 | ## 重载<<运算符 2 | 3 | 我们可以对<<运算符进行重载,这样做的好处是我们可以直接使用`cout`来进行输出。有些类似于 Java 当中实现`toString`方法。虽然概念简单,但是实际实现要稍稍复杂一些,我们一点点来说。 4 | 5 | 还是以之前的`Time`类为例,假设 trip 是`Time`类的一个对象,为了显示它的值,之前是开发了`show`函数。如果重载了<<运算符之后,我们可以这样输出: 6 | 7 | ```C++ 8 | cout << trip; 9 | ``` 10 | 11 | 显然<<是可以被重载的运算符,实际上在 C++当中,它已经被重载很多次了。 12 | 13 | <<最原本的含义是左移,这是一个位运算,`i << 1`,表示将 i 左移一位。由于 C++当中整数都以二进制存储,所以这表示`i *= 2`。`cout`是一个`ostream`对象,它能够识别所有的 C++基本类型,它对所有的基本类型都重载了`operator<<`。 14 | 15 | 现在我们要做的就是重载`Time`类中的<<运算符。 16 | 17 | ### 版本一 18 | 19 | 我们必须要使用友元函数,因为<<操作符的运算对象并不是`Time`,而是`cout`。大概是这个样子: 20 | 21 | ```C++ 22 | void operator<<(ostream &os, const Time &t) { 23 | os << t.hours << " hours, " << t.minutes << " minutes."; 24 | } 25 | ``` 26 | 27 | ### 版本二 28 | 29 | 这个版本看起来一切正常,但是有一个缺陷,就是无法执行如下的连续输出: 30 | 31 | ```C++ 32 | cout << "Trip Time: " << trip << endl; 33 | ``` 34 | 35 | 当我们使用`cout`进行连续输出`cout << x << y`,它的本质是`(cout << x) << y`。也就是说`cout << x`的返回结果同样是一个 ostream 的对象。既然如此,我们只需要将上面的版本改下即可: 36 | 37 | ```C++ 38 | ostream& operator<<(ostream &os, const Time &t) { 39 | os << t.hours << " hours, " << t.minutes << " minutes."; 40 | return os; 41 | } 42 | ``` 43 | 44 | 注意这里返回的类型是 ostream 对象的引用,因为函数开始执行的时候,程序传递的就是一个对象的引用,现在又返回回去,也就是说将传递进来的对象又返回了回去。 45 | 46 | 这样一来才算是真正实现了。 47 | -------------------------------------------------------------------------------- /C++/68-类的转换.md: -------------------------------------------------------------------------------- 1 | ## 类的转换 2 | 3 | 在 C++当中,我们经常用到类型转换。其中有一些类型是能够自行转换的。 4 | 5 | 比如: 6 | 7 | ```C++ 8 | long count = 8; 9 | double time = 11; 10 | int side = 3.33; 11 | ``` 12 | 13 | 其中 8 是`int`类型,被转换成了`long`类型,11 是`int`类型,被转换成了`double`类型。这样的转换之所以能够发生,是因为它们之间能够兼容。 14 | 15 | 有一些转换不太兼容,可能就没办法直接执行,比如: 16 | 17 | ```C++ 18 | int *p = 100; 19 | ``` 20 | 21 | 100 是一个`int`型,而等号的左边是一个`int *`是一个指针类型,两者并不兼容。虽然计算机当中的地址也是用整数来表示的,但从概念上来说,这两者不是一个东西。 22 | 23 | 这个时候如果我们要强行进行转换,就需要使用强制转换: 24 | 25 | ```C++ 26 | int *p = (int *) 10; 27 | ``` 28 | 29 | 虽然编译器并不会报错,但是显然,这样的转换并没有实际意义。在我们自定义的类当中,我们同样可以实现这样的转换。 30 | 31 | 我们来看这段代码: 32 | 33 | ```C++ 34 | class Time { 35 | private: 36 | int minutes; 37 | public: 38 | Time(); 39 | Time(int m) { 40 | minutes = m; 41 | } 42 | }; 43 | ``` 44 | 45 | 我们简化了之前例子中的`Time`类,让它只包含一个`int`。我们当然可以使用构造函数来创建对象: 46 | 47 | ```C++ 48 | Time a = Time(10); 49 | Time b(10); 50 | ``` 51 | 52 | 但这个构造函数还有一个特殊的用法,就是用在类型转换上。由于它只有一个参数`int`,所以我们可以直接将一个`int`类型转换成`Time`类的对象,like this: 53 | 54 | ```C++ 55 | Time c = 10; 56 | ``` 57 | 58 | 这里利用了 C++隐式转换的方式,除了隐式转换之外,我们也可以显式转换: 59 | 60 | ```C++ 61 | Time c = (Time) 19; 62 | ``` 63 | 64 | 有的时候,我们不希望发生这样的隐式转换怎么办?因为有的时候可能会导致一些意外的转换。因此 C++当中提供了一个新的关键字叫做`explicit`,在构造函数当中加上这个关键字之后将会关闭类的隐式转换: 65 | 66 | ```C++ 67 | class Time { 68 | private: 69 | int minutes; 70 | public: 71 | Time(); 72 | explicit Time(int m) { 73 | minutes = m; 74 | } 75 | }; 76 | ``` 77 | -------------------------------------------------------------------------------- /C++/69-转换函数.md: -------------------------------------------------------------------------------- 1 | ## 转换函数 2 | 3 | 上一篇我们聊了类的转换,C++允许通过构造函数进行隐式类型转换。 4 | 5 | 那我们自然而然产生一个问题:这样的转换可逆吗?我们有没有办法把一个类的对象再转换回基本变量类型呢? 6 | 7 | 比如: 8 | 9 | ```C++ 10 | Time t(14); 11 | int x = t; 12 | ``` 13 | 14 | 这是可以的,不过不是使用构造函数。构造函数只能用于从某种类型到类类型的转换,要进行相反的转换需要使用 C++中的一种特殊运算符函数——转换函数。 15 | 16 | 转换函数是用户定义的强制类型转换,可以使用强制类型转换的语法来使用。加入我们定义了`Time`类的转换函数,我们可以使用如下的转换: 17 | 18 | ```C++ 19 | Time t(14); 20 | int x = int(t); 21 | int y = (int) t; 22 | ``` 23 | 24 | 那么如何创建转换函数呢?其实转换函数本质上也是一种运算符重载,要转换为`typeName`类型,需要使用这种形式的转换函数: 25 | 26 | ```C++ 27 | operator typeName(); 28 | ``` 29 | 30 | 并且还有几个条件: 31 | 32 | - 必须是类方法 33 | - 不能指定返回类型 34 | - 不能有参数 35 | 36 | 加上转换类型之后,`Time`定义如下: 37 | 38 | ```C++ 39 | class Time { 40 | private: 41 | int minutes; 42 | public: 43 | Time(); 44 | Time(int m) { 45 | minutes = m; 46 | } 47 | operator int() const; 48 | operator double() const; 49 | }; 50 | ``` 51 | 52 | 虽然转换函数没有声明返回类型,但是我们一样需要返回所需的值。我们再来看下实现的代码: 53 | 54 | ```C++ 55 | Time::operator int() const { 56 | return minutes; 57 | } 58 | 59 | Time::operator double() const { 60 | return double(minutes); 61 | } 62 | ``` 63 | 64 | 到这里还没有结束,还有一个很有趣的问题,假设我们使用`cout`来输出`Time`对象,那么请问输出的结果会是`int`还是`double`呢? 65 | 66 | ```C++ 67 | Time t(14); 68 | cout << t << endl; 69 | ``` 70 | 71 | 答案是都不会,编译器会报错。因为`cout`时没有指定类型,所以编译器会报错使用了二义性(ambiguous)转换。但如果我们去掉一个转换函数,只保留一个,则不会有二义性,可以运行。 72 | 73 | 同样,我们在赋值的时候也会存在二义性: 74 | 75 | ```C++ 76 | long t = Time(14); 77 | ``` 78 | 79 | 解决办法是在赋值的时候使用强制类型转换来指出要使用哪个转换函数: 80 | 81 | ```C++ 82 | Time t(14); 83 | int x = (int) t; 84 | int y = double(t); 85 | ``` 86 | 87 | 和类的转换一样,转换函数有的时候也会有意外情况。为了避免在我们意料之外进行转换,C++11 对转换函数也支持了`explicit`关键字,加上了关键字之后,只有强制转换时才会调用这些转换函数。 88 | 89 | ```C++ 90 | class Time { 91 | private: 92 | int minutes; 93 | public: 94 | Time(); 95 | Time(int m) { 96 | minutes = m; 97 | } 98 | explicit operator int() const; 99 | explicit operator double() const; 100 | }; 101 | ``` 102 | -------------------------------------------------------------------------------- /C++/7-浮点数.md: -------------------------------------------------------------------------------- 1 | ### 浮点数 2 | 3 | 浮点数是 C++的第二组基本类型,它能够表示带小数部分的数字。不仅如此,浮点数的范围也比 int 更大,可以表示更大范围的数字。 4 | 5 | 我们都知道在计算机当中,所有数据本质上都是转化成二进制存储的。整数很简单,存储的就是转化成二进制之后的 01 串,那么浮点数又是如何存储的呢? 6 | 7 | 很容易猜到的是浮点数存储的结果也是二进制,但相比于整型直接转化成二进制要复杂一些。它需要先表示成下面这行式子: 8 | 9 | ![](https://tva1.sinaimg.cn/large/008i3skNgy1gvjqnt1ufqj60xc0guaad02.jpg) 10 | 11 | 这里的`n`即我们要存储的浮点数,`s`表示符号位,`m`是尾数,而`e`则是阶数。 12 | 13 | 符号位很好理解,它和整型当中的符号位一样,0 表示正数,1 表示负数。`m`表示尾数,$ 1\le m < 2$。我们这么看很抽象,来看一个例子,比如3.0,转化成二进制是$(11.0)\_2$,相当于$1.1*2^1$。那么,$s=1, m=1.1, e=1$。 14 | 15 | 我们了解了浮点数的表示方式,那么它又是如何存储在计算机当中的呢?这需要我们进一步地剖析其中的细节。 16 | 17 | #### 关于 m 18 | 19 | 首先是 m,m 被定义成一个大于等于 1,小于 2 的小数。我们可以简单写成 1.xx,其中 xx 表示的就是小数的部分。 20 | 21 | 既然它总是大于等于 1,小于 2 的,那么它的个位一定是 1,我们就可以将它省略,仅仅看之后小数的部分。小数的部分,我们同样使用二进制来逼近。比如 0.625,可以表示成 0.5 + 0.125,即$2^{-1} + 2^{-3}$,表示成二进制就是$(101)_2$,只不过这里它的最高位是从-1 开始的。 22 | 23 | 以 32 位的浮点数为例,除去 1 位表示符号,8 位表示阶数之后,还有 23 位留给 m。由于我们舍掉了小数点之前的 1,所以我们的阶数是从-1 开始的,理论上等价于 24 个二进制位。 24 | 25 | #### 关于 e 26 | 27 | 在浮点数存储当中,e 是一个无符号整数。以 32 位浮点数为例,e 一共有 8 位,可以表示 0-255。 28 | 29 | 但 e 是可以为负数的,根据 IEEE 754 的规定,e 的真实值必须再减去一个中间数。对于 8 位的 e,它的中间数是 127。比如 e 的实际值是 10,但是存储的时候需要存储成 127+10=137。 30 | 31 | 除此之外,e 还有另外三种情况: 32 | 33 | 1. e 不全为 0,或全为 1 时,采用上述的规则表示 34 | 2. e 全为 0 时,e 等于 1-127,有效数字 m 不再默认加上 1,这样是为了还原 0.xxx 的小数,以及接近于 0 的数 35 | 3. e 全为 1 时,如果有效数字 m 全为 0,表示无穷大,如果 m 不全为 0,表示 nan(not a number) 36 | 37 | 关于 e 的规则看起来有些复杂,初看觉得有些难以理解,为什么要用减去中间值的设计,而不用符号位?后来仔细思考了一下才发现,如果引入符号位很难区分 0.xxx 以及 e 就是等于 0 的情况,虽然也可以特判处理,但就没有现在这样优雅了。 38 | 39 | 觉得上文看不懂的小伙伴可以直接略过这段,毕竟这个是浮点数的实现原理,算是很底层的内容了,C++ primer 上对于这部分也没有过多阐述。 40 | 41 | ### 浮点数的使用 42 | 43 | C++当中有两种浮点数的书写方式,第一种是使用常规的小数点表示法: 44 | 45 | ```C++ 46 | double a = 1.23; 47 | float b = 3.43; 48 | ``` 49 | 50 | 另外一种写法是科学记数法,写成: 51 | 52 | ```C++ 53 | double a = 2.45e8; 54 | double b = 1e-7; 55 | ``` 56 | 57 | 2.45e8 表示$2.45 * 10^8$,e 之后可以跟正数也可以跟负数,但数字当中不能有空格。 58 | 59 | ### 浮点数类型 60 | 61 | 和 C 语言一样,C++也有三种浮点数类型:`float`,`double`和`long double`。和整型一样,这三种类型都是浮点数,只不过表示的范围不同。 62 | 63 | 浮点数的范围有两个部分综合决定,一个部分是有效数字。比如 14179 是 5 位有效数字,而 14000 只有两位,因为后面三个 0 都是填充位,有效数字的位数不依赖小数点的位置。C++当中要求,`float`通常能表示 7 位有效数字,`double`能表示 16 位,而`long double`至少和`double`一样。 64 | 65 | 另外,它们能够表达的指数范围至少是-37 到 37。一般来说,`float`一共是 4 个字节 32 位,而`double`是 8 个字节 64 位,当然这也取决于具体的运行环境。 66 | 67 | ### 注意事项 68 | 69 | 关于浮点数的使用有几点注意事项,千万要注意。 70 | 71 | 1. cout 输出浮点数会删除结尾的 0 72 | 2. 书写浮点数常量时默认为`double`类型,如果需要强制表示为`float`类型,请在结尾加上后缀 f 或者 F,如:`2.34f` 73 | 3. 由于浮点数有精度,不能直接判断两个浮点数是否相等,很有可能得不到预期结果,正确的做法是判断精度范围,如: 74 | 75 | ```C++ 76 | double epsilon = 1e-8; 77 | // 判断a是否和b相等 78 | if (abs(a - b) < epsilon) { 79 | // todo 80 | } 81 | ``` 82 | 83 | 判断两个浮点数 a 和 b 是否相等,等价于两者的差的绝对值小于某一个精度。 84 | 85 | 4. 范围问题,如运行下列代码将得到错误的结果: 86 | 87 | ```C++ 88 | float a = 2.3e22f; 89 | float b = a + 1.0f; 90 | 91 | cout << b - a << endl; 92 | ``` 93 | 94 | 输出的结果将是 0,因为 2.3e22 是一个小数点左边有 23 位的数字,加上 1 之后,就是在第 23 位加上 1。但是`float`类型只能表示数字中的前 6 位或者前 7 位,表示不了这么高的精度,因此这个+1 的操作完全没有生效。 95 | 96 | 这个问题是一个大坑,一不小心就会中招,千万要小心。 97 | -------------------------------------------------------------------------------- /C++/70-构造函数的一些坑.md: -------------------------------------------------------------------------------- 1 | ## 构造函数的一些坑 2 | 3 | 某一天我们接到了一个需求,需要开发一个类似于 STL 中 string 的类。我们很快写好了代码: 4 | 5 | ```C++ 6 | #include 7 | #ifndef STRINGBAD_H_ 8 | #define STRINGBAD_H_ 9 | class StringBad { 10 | private: 11 | char *str; 12 | int len; 13 | static int num_strings; 14 | public: 15 | StringBad(const char* s); 16 | StringBad(); 17 | ~StringBad(); 18 | friend std::ostream & operator << (std::ostream &os, const StringBad & st); 19 | }; 20 | #endif 21 | ``` 22 | 23 | 在这个.h 文件当中,我们定义了一个`StringBad`类,这是 C++ Primer 当中的一个例子。为什么叫 StringBad 呢,主要是为了提示,表示这是一个没有完全开发好的 demo。 24 | 25 | 这里有一个小细节,我们在类当中定义的是一个`char *`也就是字符型指针,而非字符型数组。这意味着我们在类声明当中没有为字符串本身分配空间,而是在构造函数当中使用`new`来完成的,避免了预先定义字符串的长度。 26 | 27 | 其次`num_strings`是一个静态成员,也就是说无论创建了多少对象,它都只会保存一份。类的所有成员共享同一个静态变量。 28 | 29 | 接下来我们来看一下它的实现: 30 | 31 | ```C++ 32 | #include 33 | #include "stringbad.h" 34 | 35 | using std::cout; 36 | 37 | int StringBad::num_strings = 0; 38 | 39 | StringBad::StringBad(const char* s) { 40 | len = std::strlen(s); 41 | str = new char[len+1]; 42 | std::strcpy(str, s); 43 | num_strings++; 44 | cout << num_strings << ": \"" << str << "\" object created \n"; 45 | } 46 | 47 | StringBad::StringBad() { 48 | len = 4; 49 | str = new char[4]; 50 | std::strcpy(str, "C++"); 51 | num_strings++; 52 | cout << num_strings << ": \"" << str << "\" object created \n"; 53 | } 54 | 55 | StringBad::~StringBad() { 56 | cout << "\"" << str << "\" object deleted, "; 57 | --num_strings; 58 | cout << num_strings << " left \n"; 59 | delete []str; 60 | } 61 | 62 | std::ostream & operator<<(std::ostream & os, const StringBad &st) { 63 | os << st.str; 64 | return os; 65 | } 66 | ``` 67 | 68 | 首先,我们可以注意到第一句就是将`num_strings`初始化成了 0,我们不能在类声明中初始化静态成员变量。因为声明只是描述了如何分配内存,但并不真的分配内存。 69 | 70 | 所以对于静态类成员,我们可以在类声明之外使用单独的语句进行初始化。因为静态成员变量是单独存储的,并不是对象的一部分。 71 | 72 | 初始化要在方法文件也就是 cpp 文件当中,而不是头文件中。因为头文件可能会被引入多次,如果在头文件中初始化将会引起错误。当然也有一种例外,就是加上了 const 关键字。 73 | 74 | 从逻辑上看,我们这样实现并没有任何问题,但是当我们执行的时候,就会发现问题很多…… 75 | 76 | 假设我们现在有一个函数: 77 | 78 | ```C++ 79 | void callme(StringBad sb) { 80 | cout << " \"" << sb << "\"\n"; 81 | } 82 | ``` 83 | 84 | 然后我们这么使用: 85 | 86 | ```C++ 87 | int main() { 88 | StringBad sb("test"); 89 | callme(sb); 90 | return 0; 91 | } 92 | ``` 93 | 94 | 会得到一个奇怪的结果: 95 | 96 | ![](https://tva1.sinaimg.cn/large/008i3skNgy1gxl93239vtj3120098dhe.jpg) 97 | 98 | 从屏幕可以看到我们的析构函数执行了两次,一次很好理解应该是`main`函数退出的时候自动执行的,还有一次呢?是什么时候执行的? 99 | 100 | 答案是执行`callme`函数的时候执行的,因为`callme`函数使用了值传递。当`callme`函数执行结束时,也会调用参数`sb`的析构函数。 101 | 102 | 如果我们改成引用传递,就一切正常了: 103 | 104 | ```C++ 105 | void callme(StringBad &sb) { 106 | cout << " \"" << sb << "\"\n"; 107 | } 108 | 109 | int main() { 110 | StringBad sb("test"); 111 | callme(sb); 112 | return 0; 113 | } 114 | ``` 115 | 116 | ![](https://tva1.sinaimg.cn/large/008i3skNgy1gxl96oknjij312e046wez.jpg) 117 | 118 | 这还没完,我们把代码再改一下,会发现还有问题: 119 | 120 | ```C++ 121 | int main() { 122 | StringBad sb("test"); 123 | StringBad sports("Spinach Leaves Bowl for Dollars"); 124 | StringBad sailor = sports; 125 | StringBad knot; 126 | StringBad st = sb; 127 | return 0; 128 | } 129 | ``` 130 | 131 | 执行一下,得到: 132 | 133 | ![](https://tva1.sinaimg.cn/large/008i3skNgy1gxl9bvig01j312w0c4tau.jpg) 134 | 135 | 会发现又有负数出现了,这是为什么呢? 136 | 137 | 因为我们执行了`StringBad st = sb`这样的操作,这个操作并不会调用我们实现的任何一个构造函数。它等价于: 138 | 139 | ```C++ 140 | StringBad st = StringBad(sb); 141 | ``` 142 | 143 | 对应的构造函数原型是: 144 | 145 | ```C++ 146 | StringBad(const StringBad&); 147 | ``` 148 | 149 | 当我们用一个对象来初始化另外一个对象的时候,编译器将会自动生成上述的构造函数。这样的构造函数叫做拷贝构造函数,由于我们没有重载拷贝构造函数,因此它不知道要对`num_strings`变量做处理,也就导致了不一致的发生。 150 | -------------------------------------------------------------------------------- /C++/71-拷贝构造函数.md: -------------------------------------------------------------------------------- 1 | ## 拷贝构造函数 2 | 3 | 4 | 5 | 我们上一篇文章当中聊了面向对象中的一些坑,有的时候我们命名重载了构造函数和析构函数,但还是有问题。 6 | 7 | 8 | 9 | 这是因为在C++当中除了构造函数和析构函数之外,还有一些特殊的成员函数。这些成员函数是自动定义的,当我们没有意识到它们存在的时候,往往就会出现问题。 10 | 11 | 12 | 13 | 这些成员函数有: 14 | 15 | 16 | 17 | - 默认构造函数 18 | - 默认析构函数 19 | - 拷贝构造函数 20 | - 赋值运算符 21 | - 地址运算符 22 | 23 | 24 | 25 | 编译器将会生成最后三个函数的定义——拷贝构造函数、赋值运算符和地址运算符。 26 | 27 | 28 | 29 | 比如当我们把一个对象赋值给另外一个对象的时候,编译器就会调用默认的赋值运算符,完成对象赋值工作。地址运算符返回调用对象的地址,也就是取地址的时候返回结果。一般默认返回`this`指针,我们不需要过多操作。 30 | 31 | 32 | 33 | 另外C++11当中还提供了其他两个特殊的成员函数:移动构造函数和移动赋值运算符,这个我们在后面的文章讨论。 34 | 35 | 36 | 37 | ### 默认构造函数 38 | 39 | 40 | 41 | 如果我们没有提供任何构造函数,C++将创建默认构造函数。比如我们定义了一个`Time`类,编译器将会提供如下默认构造函数: 42 | 43 | 44 | 45 | ```C++ 46 | Time::Time() {} 47 | ``` 48 | 49 | 50 | 51 | 编译器将会提供一个不接受任何参数,也不执行任何操作的构造函数。因为我们创建对象的时候总会调用构造函数: 52 | 53 | 54 | 55 | ```C++ 56 | Time t; 57 | ``` 58 | 59 | 60 | 61 | 由于默认构造函数当中没有任何的操作,因此这样创建出来的对象t中的值是未知的。如果我们创建了构造函数,那么C++不会再提供默认构造函数。如果我们希望能够继续直接创建变量,则需要我们手动提供无参构造函数。 62 | 63 | 64 | 65 | 带参数的构造函数也可以是默认构造函数,只要所有的参数都有默认值: 66 | 67 | 68 | 69 | ```C++ 70 | Time(int hours=0, int minutes=0) { 71 | _hours = hours; 72 | _minutes = minutes; 73 | } 74 | ``` 75 | 76 | 77 | 78 | 为了防止歧义,这两者不能同时出现。 79 | 80 | 81 | 82 | ### 拷贝构造函数 83 | 84 | 85 | 86 | 拷贝构造函数用于将一个对象复制到新创建的对象中。它用于初始化过程中,而不是常规的复制过程中。拷贝构造函数的原型通常如下: 87 | 88 | 89 | 90 | ```C++ 91 | Class_name(const Class_name &); 92 | ``` 93 | 94 | 95 | 96 | 它接受一个指向类对象的常量引用作为参数。 97 | 98 | 99 | 100 | 对于拷贝构造函数我们只需要知道两点:何时调用和有何功能。 101 | 102 | 103 | 104 | ### 何时调用 105 | 106 | 107 | 108 | 新建一个对象并且初始化的时候,拷贝构造函数都会被调用。大概有下面这么几种情况: 109 | 110 | 111 | 112 | ```C++ 113 | Time a; 114 | 115 | Time b(a); 116 | Time c = a; 117 | Time d = Time(a); 118 | Time *e = new Time(a); 119 | ``` 120 | 121 | 122 | 123 | 其中中间的两种声明可能会使用拷贝构造函数直接创建c和d,也可能使用拷贝构造函数生成一个临时对象,然后将临时对象的内容赋给c和d,这取决于具体的实现。 124 | 125 | 126 | 127 | 最后一种声明使用a初始化一个匿名对象,并且将匿名对象的地址赋值给e指针。 128 | 129 | 130 | 131 | 每当程序生成了对象副本,都会使用拷贝构造函数。也就是说当函数按值传递对象或函数返回对象时,都会使用拷贝构造函数。 132 | 133 | 134 | 135 | ### 默认的拷贝构造函数 136 | 137 | 138 | 139 | 默认的拷贝构造函数逐个赋值非静态成员,复制的是成员的值。比如: 140 | 141 | 142 | 143 | ```C++ 144 | Time b = a; 145 | ``` 146 | 147 | 148 | 149 | 等价于: 150 | 151 | 152 | 153 | ```C++ 154 | Time b; 155 | b.hours = a.hours; 156 | b.minutes = a.minutes; 157 | ``` 158 | 159 | 160 | 161 | 只不过`hours`和`minutes`是私有成员,无法直接访问,因此通过不了编译。如果成员本身就是一个类对象,那么将会使用这个类对象的拷贝构造函数来复制。 162 | 163 | 164 | 165 | -------------------------------------------------------------------------------- /C++/72-赋值运算符.md: -------------------------------------------------------------------------------- 1 | ## 赋值运算符 2 | 3 | 4 | 5 | C++当中允许类对象赋值,这是通过默认的重载赋值运算符实现的,它的原型如下: 6 | 7 | 8 | 9 | ```C++ 10 | Class_name & Class_name::operator=(const Class_name &); 11 | ``` 12 | 13 | 14 | 15 | 它接受并返回一个指向类对象的引用。 16 | 17 | 18 | 19 | 将已有的对象赋给另一个对象时,将会使用重载的赋值运算符: 20 | 21 | 22 | 23 | ```C++ 24 | StringBad headline1("Celery"); 25 | StringBad knot; 26 | 27 | knot = headline1; // 调用赋值运算符 28 | ``` 29 | 30 | 31 | 32 | 如果是对象初始化的过程,则不一定会使用赋值运算符,比如: 33 | 34 | 35 | 36 | ```C++ 37 | StringBad metoo = knot; 38 | ``` 39 | 40 | 41 | 42 | 像是这种情况很难说,因为`metoo`是一个新建的对象,它可以使用拷贝构造函数。然而,也可以分成两步来处理,先使用拷贝构造函数创建一个临时对象,然后在赋值的时候使用赋值运算符复制到新对象中去也是可以的。 43 | 44 | 45 | 46 | 和拷贝构造函数类似,默认赋值运算符的实现也是对成员进行逐个复制。如果成员本身就是累对象,那么会使用这个类的赋值运算符来复制。 47 | 48 | 49 | 50 | 赋值运算符的问题在哪里呢?我们还是看下之前`StringBad`那个例子,我们看下下面这段代码: 51 | 52 | 53 | 54 | ```C++ 55 | StringBad sb("test"); 56 | StringBad sports("Spinach Leaves Bowl for Dollars"); 57 | StringBad knot; 58 | knot = sports; 59 | ``` 60 | 61 | 62 | 63 | 当我们运行的时候就会遇到这样的报错: 64 | 65 | 66 | 67 | ![](https://tva1.sinaimg.cn/large/008i3skNgy1gxnjjvlq9gj312407u75i.jpg) 68 | 69 | 70 | 71 | 报错的原因日志里写得很清楚,我们尝试释放一个没有被分配的内存。 72 | 73 | 74 | 75 | 会报错的原因很简单,因为我们执行`knot = sports`的时候,两个对象内部的字符串指向的是同一个地址。这就导致了析构`knot`的时候`sports`对象对应的内容已经不存在了。 76 | 77 | 78 | 79 | 解决方案也很简单,就是我们自己重载赋值运算符,保证不会出现简单拷贝的问题。 80 | 81 | 82 | 83 | ```C++ 84 | StringBad & StringBad::operator= (const StringBad & st) { 85 | if (this == &st) return *this; 86 | delete []str; 87 | len = st.len; 88 | str = new char[len+1]; 89 | std::strcpy(str, st.str); 90 | return *this; 91 | } 92 | ``` 93 | 94 | -------------------------------------------------------------------------------- /C++/73-成员初始化列表.md: -------------------------------------------------------------------------------- 1 | ## 成员初始化列表 2 | 3 | 4 | 5 | 除了可以使用构造函数对类成员进行初始化之外,C++还提供了另外一种初始化的方法,叫做成员初始化列表。 6 | 7 | 8 | 9 | 我们假设`Classy`是一个类,而`mem1`,`mem2`和`mem3`都是这个类的数据成员,那么类构造函数可以写成: 10 | 11 | 12 | 13 | ```C++ 14 | Classy::Classy(int n, int m): mem1(n), mem2(0), mem3(n*m+2) { 15 | ... 16 | }; 17 | ``` 18 | 19 | 20 | 21 | 上述代码将`mem1`初始化为n,将`mem2`初始化为了0,`mem3`初始化成了`n*m+2`。这些初始化工作是对象创建是完成的,并且在执行花括号中的内容之前。 22 | 23 | 24 | 25 | 有这么几点需要注意: 26 | 27 | 28 | 29 | - 这种格式只能用于构造函数 30 | - 必须用这种格式初始化非静态const数据成员(C++11之前) 31 | - 必须用这种格式初始化引用数据成员 32 | 33 | 34 | 35 | 数据成员被初始化的顺序和它们在类中定义的顺序相同,和初始化列表中的排列顺序无关。 36 | 37 | 38 | 39 | ### C++11的类内初始化 40 | 41 | 42 | 43 | C++11当中允许我们直接对成员变量进行赋值: 44 | 45 | 46 | 47 | ```C++ 48 | class Classy { 49 | int mem1 = 10; 50 | const int mem2 = 20; 51 | }; 52 | ``` 53 | 54 | 55 | 56 | 这和在构造函数当中使用成员初始化列表等价: 57 | 58 | 59 | 60 | ```C++ 61 | Classy::Classy() : mem1(0), mem2(20) {...} 62 | ``` 63 | 64 | 65 | 66 | 我们在类当中直接初始化之后,我们也可以在成员初始化列表当中进行覆盖: 67 | 68 | 69 | 70 | ```C++ 71 | Classy::Classy(int n) : mem1(n) {...} 72 | ``` 73 | 74 | 75 | 76 | 在这种情况下,`mem1`的值会被替换成n。 -------------------------------------------------------------------------------- /C++/74-继承(一).md: -------------------------------------------------------------------------------- 1 | ## 继承(一) 2 | 3 | 4 | 5 | 在我们进行开发的时候,我们经常会遇到抽象出来的类之间具有继承关系。 6 | 7 | 8 | 9 | 举个简单的例子,比如我们在设计某游戏,当中需要定义`Human`也就是人这个类。每个人有名字,以及一定的血量,能够工作。也就是说`Human`这个类具有名字和血量这两个成员变量,还有一个工作的函数。 10 | 11 | 12 | 13 | 现在我们还需要开发一个英雄`Hero`类,英雄也是人,他应该也有名字和血量,以及也可以工作。但英雄又和普通人不同,他具有一些特殊的属性。比如变异,比如超能力等等。那么我们在开发`Hero`这个类的时候,绝大多数的功能都和`Human`一样,但是又需要额外开发一些超能力函数。 14 | 15 | 16 | 17 | 这个时候我们就会很自然地想到,如果`Hero`类能够复用`Human`类当中的内容,那么只要单独额外开发超能力相关的功能即可。 18 | 19 | 20 | 21 | 让一个类能够复用另外一个类当中所有的功能,这样的功能叫做继承。在日常开发当中,类似的需求反复出现,因此继承是面向对象当中非常重要的一个部分。 22 | 23 | 24 | 25 | 一个类继承了另外一个类,被继承的类成为基类或父类,继承的类成为子类或派生类。 26 | 27 | 28 | 29 | 为了更好地说明,我们来看一个例子: 30 | 31 | 32 | 33 | ```C++ 34 | class Human { 35 | private: 36 | string _name; 37 | int _hp; 38 | int _property; 39 | public: 40 | Human(const string & name = "", const int hp = 100, const int property = 0): _name(name), _hp(hp), _property(property) {} 41 | void Name() const { 42 | return _name; 43 | } 44 | void work(int salary) { 45 | _property += salary; 46 | } 47 | }; 48 | ``` 49 | 50 | 51 | 52 | 我们简单实现了`Human`这个类,给它赋予了三个属性,分别是name名称,hp血量,property和财产。以及三个函数,分别是构造函数、获取姓名以及工作。 53 | 54 | 55 | 56 | 现在我们想要实现一个英雄`Hero`类,它首先要继承`Human`类,我们可以这么写: 57 | 58 | 59 | 60 | ```C++ 61 | class Hero : public Human { 62 | ... 63 | }; 64 | ``` 65 | 66 | 67 | 68 | 冒号表示继承,冒号之后的类为继承的父类,`public`表明这是一个共有基类,这被称为共有派生,派生类对象包含基类对象。使用共有派生,基类的公有成员将称为派生类的公有成员,基类私有部分也将称为派生类的一部分,但只能通过基类的公有和保护方法访问。 69 | 70 | 71 | 72 | 关于这一部分当中的细节,我们将会在之后的文章当中详细解释。目前我们只需要知道,这样的写法可以实现一个共有派生,以及共有派生的相关内容即可。 73 | 74 | 75 | 76 | 比如英雄有超能力,我们需要一个超能力的名字,还需要一个函数使用超能力,那么就可以写成这样: 77 | 78 | 79 | 80 | ```C++ 81 | class Hero : public Human { 82 | private: 83 | string _super_power; 84 | public: 85 | Hero(const string &name = "", const int hp = 100, const property = 0, const string & sp): Human(name, hp, property), _super_power(sp) {} 86 | string SuperPower() const { 87 | return _super_power; 88 | } 89 | void use_power() { 90 | cout << _super_power << endl; 91 | } 92 | }; 93 | ``` 94 | 95 | 96 | 97 | 这里面有一个细节,派生类不能直接访问基类的私有成员,必须要通过基类的方法进行访问。构造函数也是一样,因此派生类必须要使用积累的构造函数。 98 | 99 | 100 | 101 | 创建派生类的时候,程序会先创建基类对象,基类对象会在程序进入派生类之前被创建。所以我们也可以使用列表初始化的方法来完成: 102 | 103 | 104 | 105 | ```C++ 106 | Hero(const string &name = "", const int hp = 100, const property = 0, const string & sp): Human(name, hp, property), _super_power(sp) {} 107 | ``` 108 | 109 | 110 | 111 | 如果我们去除代码当中的`Human(name, hp, property)`,那么程序会调用`Human`类的默认构造函数创建一个默认对象。 112 | 113 | 114 | 115 | 当然,我们也可以创建一个入参是父类的构造函数,这样也是可以的。 116 | 117 | 118 | 119 | ```C++ 120 | Hero::Hero(const Human & hu, const string & sp): Human(hu), _super_power(sp) {} 121 | ``` 122 | 123 | -------------------------------------------------------------------------------- /C++/75-继承(二).md: -------------------------------------------------------------------------------- 1 | ## 继承(二) 2 | 3 | 4 | 5 | 在实际编程当中,父类和子类之间会存在一些特殊的关键。 6 | 7 | 8 | 9 | 比如子类可以使用父类所有的`public`方法,我们来看一下之前的例子: 10 | 11 | 12 | 13 | ```C++ 14 | class Human { 15 | private: 16 | string _name; 17 | int _hp; 18 | int _property; 19 | public: 20 | Human(const string & name = "", const int hp = 100, const int property = 0): _name(name), _hp(hp), _property(property) {} 21 | void Name() const { 22 | return _name; 23 | } 24 | void work(int salary) { 25 | _property += salary; 26 | } 27 | }; 28 | 29 | class Hero : public Human { 30 | private: 31 | string _super_power; 32 | public: 33 | Hero(const string &name = "", const int hp = 100, const property = 0, const string & sp): Human(name, hp, property), _super_power(sp) {} 34 | string SuperPower() const { 35 | return _super_power; 36 | } 37 | void use_power() { 38 | cout << _super_power << endl; 39 | } 40 | }; 41 | 42 | Hero hero("peter", 100, 0, "spider"); 43 | hero.Name(); 44 | ``` 45 | 46 | 47 | 48 | 在这个例子当中`Name`是父类`Human`当中定义的,但对于子类的对象来说,它一样有权限可以使用。 49 | 50 | 51 | 52 | 除此之外还有两个特殊的关系,首先是父类的指针可以不进行显式类型转换的情况下指向子类对象: 53 | 54 | 55 | 56 | ```C++ 57 | Hero spider("peter", 100, 100, "spider"); 58 | Human *h = &spider; 59 | ``` 60 | 61 | 62 | 63 | 同样,父类引用也可以在不进行强制类型转换的情况下引用子类对象: 64 | 65 | 66 | 67 | ```C++ 68 | Human &sp = spider; 69 | ``` 70 | 71 | 72 | 73 | 一般来说C++会要求引用和指针与实际获得的类型匹配,但对于继承类来说是一个例外。不过这种例外是单向的,也就是说我们只能用父类指针或引用去接收子类对象,反过来是不行的。 74 | 75 | 76 | 77 | 除了赋值的时候更加方便之外,我们在传参的时候也会更加方便。我们在接收函数的参数设置成父类,那么无论是父类还是子类的实例都可以传入。 78 | 79 | 80 | 81 | ```C++ 82 | void show(Human &h) { 83 | ... 84 | } 85 | ``` 86 | 87 | -------------------------------------------------------------------------------- /C++/76-继承(三).md: -------------------------------------------------------------------------------- 1 | ## 继承(三) 2 | 3 | 4 | 5 | 通过我们之前几篇关于继承的文章,相信大家也注意到了,派生类和基类之间存在一种特殊的关系。 6 | 7 | 8 | 9 | 这种关系非常特殊,今天这篇文章着重聊一下两者之间的关系。 10 | 11 | 12 | 13 | 在C++当中,继承的方式有3种,分别是公有继承、保护继承和私有继承。这三种继承方式类似,只是成员变量的权限不同,我们一一来说。 14 | 15 | 16 | 17 | 首先是公有继承,它的特点是基类的公有成员和保护成员被继承是都保持原有状态,基类的私有成员仍然是私有的,不能被派生类访问。 18 | 19 | 20 | 21 | 其次是保护继承,它的特点是基类的所有公有成员和保护成员都成为派生类的保护成员,并且只能被它的派生类成员函数或友元访问,基类的私有成员仍然是私有的。 22 | 23 | 24 | 25 | 最后是私有继承,私有继承的特点是基类的公有成员和保护成员都作为派生类的私有成员,并且不能被这个派生类的子类所访问。 26 | 27 | 28 | 29 | 我们可以用一张图来进行总结: 30 | 31 | 32 | 33 | ![](https://tva1.sinaimg.cn/large/008i3skNgy1gxs5rj1j4xj30ri09gdgi.jpg) 34 | 35 | 36 | 37 | 所以继承的逻辑是一样的,只不过权限划分不同。 38 | 39 | 40 | 41 | 在这3种继承当中,最常用的是公有继承,它建立的是一种is-a关系。即派生类对象也是一个基类对象,基类对象可以执行的操作也可以对派生类执行。 42 | 43 | 44 | 45 | 比如说我们有一个类是`Fruit`,还有一个类是`Apple`,显然苹果也是水果,所以水果有的属性苹果应该都有,苹果可能有一些特殊的属性,比如热量、品种等等,这些属性不一定适用于所有水果。所以苹果和水果之间的关系就是is-a,可以理解成is-a-kind-of,简单写成is-a。 46 | 47 | 48 | 49 | 也就是说苹果是水果,但水果不一定是苹果,这样的一种从属关系。 50 | 51 | 52 | 53 | 我们再来看一些反例,比如说午餐和水果之间的关系。午餐当中可能有水果,但午餐本质不是水果。所以我们不能从`Fruit`类派生到`Lunch`。正确的说法是午餐当中包含了水果,水果是午餐的一个成员。它们之间的关系是has-a的关系。 54 | 55 | 56 | 57 | 再来看另外一个错误答案,比如水面像是镜子,但水面不是镜子,镜子也不是水面。因此不能从`Water`类派生出`Mirror`类。这个相对比较明显,大家应该都能注意到。这两者之间的关系叫做is-like-a。 58 | 59 | 60 | 61 | 再来看一个不是那么明显的,叫做is-implemented-as-a(作为...来实现)关系。比如我们可以用数组实现栈,但数组不是栈,栈也不是数组。所以我们不能从`Array`类来派生出`Stack`类。 62 | 63 | 64 | 65 | 再比如uses-a的关系,计算机可以使用打印机,但`Computer`类不能派生出`Printer`类,这是没有意义的。可以将打印机作为计算机的一个成员变量。 66 | 67 | 68 | 69 | 虽然在C++当中我们并没有严格的限制,我们完全可以通过继承是来实现has-a, is-implemented-as-a, uses-a关系。但这样的做法通常会导致一些问题,因此我们在开发的时候需要保持谨慎,坚持使用is-a关系。 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /C++/77-多态.md: -------------------------------------------------------------------------------- 1 | ## 多态 2 | 3 | 4 | 5 | 在我们之前介绍的继承的情况当中,派生类调用基类的方法都是不做任何改动的调用。 6 | 7 | 8 | 9 | 但有的时候会有一些特殊的情况,我们会希望同一个方法在不同的派生类当中的行为是不同的。举个简单的例子,比如`speak`方法,在不同的类当中的实现肯定是不同的。如果是`Human`类,就是正常的说话,如果是`Dog`类可能是汪汪,而`Cat`类则是喵喵。 10 | 11 | 12 | 13 | 在这种情况下只是简单地使用继承是无法满足我们的要求的,最好能够有一个机制可以让方法有多种形态,不同的对象去调用的逻辑不同。这样的行为称为多态。 14 | 15 | 16 | 17 | 这里稍微强调一下,多态是一种面向对象的设计思路,本身和C++不是强绑定的,其他语言当中一样有多态,只不过实现的方式可能有所不同。 18 | 19 | 20 | 21 | 在C++当中有两种重要的机制用于实现多态: 22 | 23 | 24 | 25 | - 在派生类当中重新定义基类的方法 26 | - 使用虚方法 27 | 28 | 29 | 30 | 我们来看一个例子: 31 | 32 | 33 | 34 | ```C++ 35 | class Mammal { 36 | private: 37 | string name; 38 | public: 39 | Mammal(string n): name(n) {} 40 | string Name() const{ 41 | return name; 42 | } 43 | virtual void speak() const { 44 | cout << "can't say anything" << endl; 45 | } 46 | virtual ~Mammal() {}; 47 | }; 48 | 49 | class Human : public Mammal{ 50 | private: 51 | string job; 52 | public: 53 | Human(string n, string j): Mammal(n), job(j) {} 54 | virtual void speak() const { 55 | cout << "i'm human" << endl; 56 | } 57 | }; 58 | ``` 59 | 60 | 61 | 62 | 由于示例比较简单,所以我们把类的声明和实现写在一起了。 63 | 64 | 65 | 66 | 从结构上来看,这就是一个简单的继承,我们实现了两个类,一个叫做`Mammal`,一个叫做`Human`,然后给它们各自定义了一些成员变量。 67 | 68 | 69 | 70 | 值得注意的是`speak`函数,我们在函数声明前面加上了一个关键字`virtual`,这表示这是一个虚函数。 71 | 72 | 73 | 74 | 方法被定义成虚方法之后,在程序执行的时候,将会根据派生类的类型来选择执行的方法版本。在进行调用的时候,程序是根据对象类型而不是引用和指针的类型来选择执行的方法,如: 75 | 76 | 77 | 78 | ```C++ 79 | Mammal *m = new Human("man", "spiderman"); 80 | m->speak(); 81 | ``` 82 | 83 | 84 | 85 | 这里我们用一个`Mammal`的指针指向了一个`Human`类型的对象,当我们调用方法的时候,由于`speak`方法是一个虚方法。因此执行的时候程序会根据对象的类型也就是`Human`去执行`Human`对象中的`speak`方法,而不是`Mammal`中的。 86 | 87 | 88 | 89 | 通常我们会将析构函数也设置成虚方法,因为派生类当中往往有一些专属成员,这是一种惯例。因为如果析构函数不是虚函数,那么只会调用对应指针类型的析构函数,这意味着可能在一些情况下产生错误和问题。 90 | 91 | 92 | 93 | 在上述的示例当中,我们是将类方法的实现和声明写在一起了,如果还是采取和之前一样分开实现的方式,需要注意我们无需在函数签名中加上`virtual`关键字。 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /C++/78-静态联编和动态联编.md: -------------------------------------------------------------------------------- 1 | ## 静态联编和动态联编 2 | 3 | 4 | 5 | 当我们使用程序调用函数的时候,究竟应该执行哪一个代码块呢?将源代码中的函数调用解释为执行特定的函数代码块这个过程被称为函数名联编(binding)。 6 | 7 | 8 | 9 | 在C语言当中,这非常简单,因为每个函数名都对应一个不同的函数。而在C++当中,由于支持了函数重载,使得这个任务变得更加复杂。编译器必须要查看函数的参数以及函数名才能确定。好在函数的选择以及参数在编译的时候都是确定的,所以这部分联编在编译时就能完成,这种联编被称为静态联编。 10 | 11 | 12 | 13 | 在有了虚函数之后, 这个工作变得更加复杂。因为使用哪一个函数不能在编译时确定了,因为编译器不知道用户将选择哪个类型的对象。所以,编译器必须能在程序运行的时候选择正确的虚函数,这被称为动态联编。 14 | 15 | 16 | 17 | ### 指针和引用类型的兼容性 18 | 19 | 20 | 21 | 在C++当中,动态联编与指针和引用调用方法相关,这是由继承控制的。前文当中说过,公有继承建立的is-a关系,使得我们可以用父类指针或引用指向子类的对象。而在C++当中,是不允许将一种类型的地址赋值给另外一种类型的指针的。 22 | 23 | 24 | 25 | 下面两种操作都是非法的。 26 | 27 | 28 | 29 | ```C++ 30 | double x = 2.5; 31 | int *pi = &x; // 非法 32 | long &r = x; // 非法 33 | ``` 34 | 35 | 36 | 37 | 将派生类引用或指针转换成基类的引用和指针称为向上强制转换(upcasting),这种规则是is-a关系的一部分。因为派生类继承了基类当中所有的数据成员和成员函数,因此基类成员能够进行的操作都适用于子类成员,所以向上强制转换是可传递的。 38 | 39 | 40 | 41 | 如果反过来呢?将父类对象传递给子类指针呢?这种操作被称为向下强制转换(downcasting),在不使用强制转换的前提下是不允许的。因为is-a关系通常是不可逆的,派生类当中往往新增了一些数据成员或方法,不能保证在父类对象上一样还能兼容。 42 | 43 | 44 | 45 | ### 虚函数的工作原理 46 | 47 | 48 | 49 | 我们在使用虚函数的时候其实可以不需要知道当中的实现原理,但是了解了工作原理能够帮助我们更好地理解理念。另外在C++相关的开发面试当中经常会问起类似的实现细节。 50 | 51 | 52 | 53 | 通常,编译器处理虚函数的方法是:给每一个对象添加一个隐藏成员,这个成员当中保存了一个指向函数地址数组的指针,这种数组称为虚函数表。 54 | 55 | 56 | 57 | 这个虚函数表中存储了当前这个类对象的声明的虚函数的地址,我们来看一个例子: 58 | 59 | 60 | 61 | ```C++ 62 | class Human { 63 | private: 64 | ... 65 | char name[40]; 66 | public: 67 | virtual void show_name(); 68 | virtual void show_all(); 69 | ... 70 | }; 71 | 72 | class Hero : public Human{ 73 | private: 74 | ... 75 | char power[20]; 76 | public: 77 | void show_all(); 78 | virtual void show_power(); 79 | ... 80 | }; 81 | ``` 82 | 83 | 84 | 85 | 对于`Human`类型的对象,它当中除了类中声明的内容之外,还会额外多一个指针,指向一个列表,比如是`[1024,1222]`。 86 | 87 | 88 | 89 | 这里的1024和1222分别是`show_name`和`show_all`两个函数代码块的地址。 90 | 91 | 92 | 93 | 同样`Hero`子类当中也会有这样一个指针指向一个虚函数的列表,由于我们在`Hero`子类当中没有重载`show_name`方法,所以`Hero`类型的对象中的列表中的第一个元素仍然是1024。由于我们重载了`show_all`方法,以及我们新增了一个`show_power`的虚函数,因此它的虚函数列表可能是`[1024,2333,3777]`。 94 | 95 | 96 | 97 | 简单来说,当我们调用虚函数的时候, 编译器会先通过每个对象中的虚函数列表指针拿到虚函数列表。然后在找到对应位置的虚函数代码块的地址,最后进行执行。 98 | 99 | 100 | 101 | 显然这个过程涉及到维护虚函数地址表,以及函数执行时有额外的查表操作,既带来了存储空间的消耗,也带来了性能的消耗。 102 | 103 | -------------------------------------------------------------------------------- /C++/79-虚函数.md: -------------------------------------------------------------------------------- 1 | ## 虚函数注意事项 2 | 3 | 4 | 5 | 在之前的文章当中,我们已经讨论了虚函数的使用方法,也对它的原理进行了简单的介绍。 6 | 7 | 8 | 9 | 这里简单做一个总结: 10 | 11 | 12 | 13 | - 在基类的方法声明中使用关键字`virtual`可以声明虚函数 14 | - 加上了`virtual`关键字的函数在基类以及派生类和派生类再派生出来的类中都是虚的 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 | 所以通常我们会将析构函数设置成`virtual`,即使不用做基类也不会引起错误,至多只会影响一点效率。但在大型合作开发的项目当中,许多组件和类都是共享的,我们往往无法保证我们开发的类是否会被其他开发者继承,因此设置虚析构函数也是一种常规做法。 49 | 50 | 51 | 52 | ### 友元 53 | 54 | 55 | 56 | 友元函数不能是虚函数,因为友元不是类成员,只有成员函数才能是虚函数。 57 | 58 | 59 | 60 | 如果我们希望友元函数也能实现类似虚函数的功能, 我们可以在友元函数当中使用虚函数来解决。 61 | 62 | 63 | 64 | ### 没有重新定义 65 | 66 | 67 | 68 | 如果派生类当中没有重新定义虚函数,那么将使用该函数的基类版本。如果派生类位于派生链中,如B继承了A,C继承了B这种情况,那么派生类将会使用最新的虚函数版本。 69 | 70 | 71 | 72 | ### 重新定义将隐藏方法 73 | 74 | 75 | 76 | 我们来看一个例子: 77 | 78 | 79 | 80 | ```C++ 81 | class Mammal { 82 | private: 83 | string name; 84 | public: 85 | Mammal(string n): name(n) {} 86 | virtual void speak() const { 87 | cout << "can't say anything" << endl; 88 | } 89 | }; 90 | 91 | class Human : public Mammal{ 92 | private: 93 | string job; 94 | public: 95 | Human(string n, string j): Mammal(n), job(j) {} 96 | virtual void speak(const string st) const { 97 | cout << "i'm human" << endl; 98 | } 99 | }; 100 | ``` 101 | 102 | 103 | 104 | 我们在父类当中定义了一个无参虚函数`speak`,而在子类`Human`当中也定义了一个需要传入一个`string`类型的虚函数`speak`。 105 | 106 | 107 | 108 | 我试了一下,在我的g++编译器当中,会报错: 109 | 110 | 111 | 112 | ![](https://tva1.sinaimg.cn/large/008i3skNgy1gxwq7xtfo4j30xi06y0t8.jpg) 113 | 114 | 115 | 116 | 但根据C++ Primer中的说法,在一些古老的编译器当中,可能不会报错,甚至可能连警告都没有。 117 | 118 | 119 | 120 | 在这类编译器当中,我们重新定义父类中的虚函数,这样的重新定义不会生成两个重载版本,而是隐藏了父类无参的版本,只保留了接受`string`类型的版本,这种情况有别于函数重载。 121 | 122 | 123 | 124 | 在派生类当中重新定义函数,不是使用相同的函数特征标覆盖基类声明,而是隐藏同名的基类方法,不管函数特征标如何。 125 | 126 | 127 | 128 | C++ Primer当中给出了两条经验规则: 129 | 130 | 131 | 132 | - 如果重新定义继承的方法,应该保证与原来的原型完全相同,唯一的例外是返回的类型,如果基类返回的是基类的引用或指针,派生类可以改成派生类的引用或指针: 133 | 134 | 135 | 136 | ```C++ 137 | class Mammal { 138 | private: 139 | string name; 140 | public: 141 | Mammal(string n): name(n) {} 142 | virtual Mammal* build(); 143 | }; 144 | 145 | class Human : public Mammal{ 146 | private: 147 | string job; 148 | public: 149 | Human(string n, string j): Mammal(n), job(j) {} 150 | virtual Human* build(); 151 | }; 152 | ``` 153 | 154 | 155 | 156 | - 如果基类声明被重载了,那么应该在派生类中声明所有的基类版本: 157 | 158 | 159 | 160 | ```C++ 161 | class Mammal { 162 | private: 163 | string name; 164 | public: 165 | Mammal(string n): name(n) {} 166 | virtual void speak() const ; 167 | virtual void speak(int n) const; 168 | virtual void speak(const string st) const; 169 | }; 170 | 171 | class Human : public Mammal{ 172 | private: 173 | string job; 174 | public: 175 | Human(string n, string j): Mammal(n), job(j) {} 176 | virtual void speak() const ; 177 | virtual void speak(int n) const; 178 | virtual void speak(const string st) const; 179 | }; 180 | ``` 181 | 182 | 183 | 184 | 如果我们只重新定义了一个版本,那么另外两个版本将隐藏。 185 | 186 | 187 | 188 | 但这可能和编译器版本有关,在新版的编译器当中似乎取消了这一设定。 189 | 190 | 191 | 192 | 我尝试了一下,发现并不会隐藏,一样可以顺利调用父类方法。 193 | 194 | 195 | 196 | ```C++ 197 | class Mammal { 198 | private: 199 | string name; 200 | public: 201 | Mammal(string n): name(n) {} 202 | virtual void speak() const { 203 | cout << "can't say anything from empty" << endl; 204 | } 205 | virtual void speak(const string st) const { 206 | cout << "can't say anything from string input" << endl; 207 | } 208 | }; 209 | 210 | class Human : public Mammal{ 211 | private: 212 | string job; 213 | public: 214 | Human(string n, string j): Mammal(n), job(j) {} 215 | virtual void speak(const string st) const { 216 | cout << "i'm human" << endl; 217 | } 218 | }; 219 | 220 | int main() { 221 | Mammal *m = new Human("man", "spiderman"); 222 | m->speak(); 223 | return 0; 224 | } 225 | ``` 226 | 227 | 228 | 229 | -------------------------------------------------------------------------------- /C++/8-算术运算符与类型转换.md: -------------------------------------------------------------------------------- 1 | ### 算术运算符 2 | 3 | C++当中提供 5 种基础的算术运算符:加法、减法、乘法、除法和取模。 4 | 5 | 我们来看下代码: 6 | 7 | ```C++ 8 | int a = 10, b = 3; 9 | 10 | cout << a + b << endl; // 13 11 | cout << a - b << endl; // 7 12 | cout << a * b << endl; // 30 13 | cout << a / b << endl; // 3 14 | cout << a % b << endl; // 1 15 | ``` 16 | 17 | 前面三个都非常简单,着重讲下最后两种。 18 | 19 | 对于除法来说,我们要注意的是它是区分类型的。当我们的除数和被除数都是整数的时候,得到的结果也会是一个整数。所以 10 ➗ 3 得到的结果就是 3,它的小数部分会被抛弃。想要得到小数结果,只需要除数或者被除数当中有一个是浮点型即可。 20 | 21 | 取模运算符求的就是一个数除以另外一个数之后的余数。这里要注意,在其他语言当中并没有对取模运算的限制,而在 C++当中,严格限制了取模运算的对象只能是整数。否则编译的时候会报错: 22 | 23 | ![](https://tva1.sinaimg.cn/large/008i3skNgy1gvkaskc7y9j613g04w74u02.jpg) 24 | 25 | ### 优先级 26 | 27 | C++当中算术运算符的优先级和我们从小数学课本里是一样的,先乘除再加减。 28 | 29 | 如: 30 | 31 | ```C++ 32 | 3 + 4 * 5; // 23 33 | 120 / 4 * 5; // 150 34 | 20 * 5 + 4 * 6; // 124 35 | ``` 36 | 37 | 即当乘除法和加减法同时出现时,先算乘除后算加减。如果有多个运算符同样优先级,那么先左后右。 38 | 39 | ### 类型转换 40 | 41 | 前面说了,同样是除法,根据除数和被除数类型的不同,得到的结果也不同。这样固然非常灵活,但是除了更加复杂给学习、使用者带来负担之外,也会使得计算机的操作更加复杂。 42 | 43 | 比如我们一共有 11 种整型和 3 种浮点型,那么我们在计算的时候就会出现大量不同的情况。比如 short + short,short + int,short + double 等等,那么编译器就需要对这么多种情况都进行处理,这显然是非常麻烦的。为了解决这个问题,C++会自动执行许多类型转换。 44 | 45 | 下面我们对这些情况进行一一讨论。 46 | 47 | - 初始化和赋值时的转换 48 | 49 | 当我们对某个值进行初始化或者赋值的时候,C++会自动将赋予的值转化成接收者的类型。比如: 50 | 51 | ```C++ 52 | float a = 3.5f; 53 | double b = a; 54 | ``` 55 | 56 | 在上面这个例子当中,我们将一个`float`类型的变量 a 赋值给了`double`类型的 b。那么编译器会将 a 的值拓展成 64 位的`double`再赋值给 b。也就是说不会影响 b 的类型。 57 | 58 | 这样将长度更短的变量转化成更长变量的类型转换除了多占用一点内存之外,不会导致什么问题。但反向操作可能就会出错,比如: 59 | 60 | ```C++ 61 | long long a = 0x3f3f3f3f3f3f3f; 62 | int b = a; 63 | ``` 64 | 65 | 在上面的例子当中,我们将一个`long long`赋值给了`int`,由于 a 的数值非常大超过了`int`能够承载的范围,进行这样的赋值之后,编译器并不会报错(甚至不会有警告),但将会导致结果错误。b 变量将不可能再和 a 变量相等。 66 | 67 | 再比如将`float`变量赋值给`int`的时候,同样也会有类似的问题,所以在进行赋值的时候,当两个变量的类型不同时,千万要当心。 68 | 69 | - 使用花括号进行转换 70 | 71 | 这是 C++ 11 的新特性,使用大括号进行初始化,这种操作被称为列表初始化。 72 | 73 | 这种方式的好处和坏处都很明显,好处是它不允许变量长度缩窄的情况,坏处则是又增加了学习的成本。例如,不允许将浮点型转换成整型。在不同的整型之间以及整型转化成浮点型的操作可能被允许,取决于编译器知道目标变量能够正确地存储赋给它的值。比如可以将`int`类型赋值给`long`,因为`long`总是至少与`int`一样长,反向操作则会被禁止。 74 | 75 | ```C++ 76 | int a = 0x3f3f3f3f; 77 | long b = {a}; // 允许 78 | 79 | long a = 0x3f3f3f3f; 80 | int b = {a}; // 禁止 81 | ``` 82 | 83 | 关于列表初始化,C++ primer 当中还列举了一个非常有意思的 case: 84 | 85 | ```C++ 86 | const int x = 55; 87 | char c = {x}; // 允许 88 | 89 | int x = 55; 90 | char c = {x}; // 禁止 91 | 92 | const int x = 1255; 93 | char c = {x}; // 禁止 94 | 95 | const int x = 1255; 96 | char c = x; // 允许会警告 97 | ``` 98 | 99 | 这是为什么呢?因为我们加了 const 修饰之后,编译器就明确知道了 x 的值,就等于 55,它在`char`类型的范围内,所以允许将它转化成`char`。如果不加 const,那么在编译器看来 x 是一个`int`型的变量,它的范围要大于`char`,所以会禁止。即使我们加了 const 修饰,如果 x 的值过大,超过`char`的范围,也同样会被禁止。 100 | 101 | ### 表达式中转换 102 | 103 | 当一个表达式当中出现多个变量类型的时候,C++也会进行转换。由于可能涉及的情况非常多,使得这个转换的规则也会比较复杂。 104 | 105 | 1. 表达式时 C++会将`bool`、`char`、`unsigned char`、`signed char`和`short`全部转换为`int` 106 | 107 | 对于 bool 类型来说,`true`会被转化成 1,`false`转换成 0,其他类型的转换应该都很好理解,都是将范围更小的变量转化成范围更大的`int`,这种转换称作整型提升。因为通常`int`类型都是计算机最自然的类型,也意味着计算机在处理`int`的时候,处理的速度最快。 108 | 109 | 将不同类型进行运算的时候,也会做一些转换。比如将`int`和`float`相加的时候,由于涉及到两种类型,其中范围较小的那个会被转换成较大的类型。比如如果我们计算`9.0 / 5`,那么编译器会先将 5 转化成 5.0,再进行除法运算,这样得到的结果自然也是一个`double`。 110 | 111 | C++11 的规范中除了一个类型转换的校验表,我们可以参考一下校验表理解一下类型转换的过程。 112 | 113 | 1. 如果有一个数类型是`long double`,则将另外一个数也转成`long double` 114 | 2. 否则,如果有一个数类型是`double`,则将另外一个数也转成`double` 115 | 3. 否则,如果有一个数类型是`float`,则将另外一个数也转成`float` 116 | 4. 否则说明所有操作数都是整数,执行整型提升 117 | 118 | ### 强制类型转换 119 | 120 | C++当中允许开发者手动强制对变量的类型进行转换,这也是 C++的设计思路,规则严谨,但也允许推翻规则追求灵活度。 121 | 122 | 强制类型转换的方式有两种写法: 123 | 124 | ```C++ 125 | int a; 126 | (long) a; 127 | long (a); 128 | ``` 129 | 130 | 这两行代码都是将一个`int`型的 a 转换成`long`型的,上面的是 C 语言的写法,底下一行是 C++的写法。 131 | 132 | 还有一点要注意就是转换的顺序,我们来看一个例子: 133 | 134 | ```C++ 135 | int a = 11.99 + 19.99; 136 | cout << a << endl; 137 | int b = int(11.99) + int(19.99); 138 | cout << b << endl; 139 | ``` 140 | 141 | 在这段代码当中 a 和 b 输出的结果是不同的,a 输出的结果是 31,而 b 是 30。 142 | 143 | 这是因为第一行代码是先计算的加法,得到 31.98,再通过类型转换将 31.98 转换成`int`。对于浮点数向整型的转换,C++会直接抹掉小数部分,所以得到的结果是 31。而第二行代码当中,我们是先进行的类型转换,11.99 和 19.99 分别被转换成了 11 和 19,相加得到的结果也就是 30 了。 144 | 145 | 这里的一点差别很多新人经常踩坑,千万注意。 146 | -------------------------------------------------------------------------------- /C++/80-protected.md: -------------------------------------------------------------------------------- 1 | ## 访问控制protected 2 | 3 | 4 | 5 | 我们之前介绍了public和private关键字,但除了这两个之外,还存在另外一个控制权限的关键字,叫做protected。 6 | 7 | 8 | 9 | 关键字protected与private相似,在类外只能用公有类成员来访问protected成员。只有在派生的时候,private和protected关键字才能体现出差异来,派生类的成员可以直接访问基类的保护成员,但不能直接访问基类的私有成员。因此对于类外部来说,protected和private类似,而对于派生类来说,protected与public类似。 10 | 11 | 12 | 13 | 比如类`Human`类将`name`成员设置为protected: 14 | 15 | 16 | 17 | ```C++ 18 | class Human { 19 | protected: 20 | string name; 21 | ... 22 | }; 23 | ``` 24 | 25 | 26 | 27 | 在这种情况下它的派生类`Hero`可以直接访问`name`,而不需要使用`Human`中的方法: 28 | 29 | 30 | 31 | ```C++ 32 | class Hero: public Human { 33 | void show() { 34 | cout << name << endl; 35 | } 36 | }; 37 | ``` 38 | 39 | 40 | 41 | 使用protected关键字在一些情况下可以简化代码的编写工作,但也会存在一些设计缺陷。比如有的时候,有些变量值我们也不希望派生类能够直接修改。 42 | 43 | 44 | 45 | ```C++ 46 | void Hero::modify(const string& n) { 47 | name = n; 48 | } 49 | ``` 50 | 51 | 52 | 53 | 比如原本`name`名字是初始化之后不允许修改的,但由于它被定义成了protected,所以在派生类当中可以随意修改,这显然违背了我们的设计初衷。 54 | 55 | 56 | 57 | 针对这种情况,比较好的做法是将所有的数据成员都设置成private。但是可以将一些特殊的方法设置成protected,这样既可以允许派生类调用这些protected的方法得到便利,又不会过度开放权限导致敏感数据被篡改。 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /C++/9-数组的定义和初始化.md: -------------------------------------------------------------------------------- 1 | ### 数组 2 | 3 | 数组其实也是一种数据格式,不过是一种复合类型,它可以存储多个同类型的值。 4 | 5 | 使用数组可以将同类型的变量整合起来管理,比如说我们现在要记录三个同学的考试得分。我们当然可以写成`int a1, a2, a3;`,看起来也不会很麻烦。但如果我们有 50 个同学呢?如果有 5000 个同学呢?显然就不能通过这种方式了,何况每个变量都要起一个独一无二的名字,这也很麻烦。 6 | 7 | 使用数组就不会有这样的问题,我们只需要规定数组的长度,通过一个变量就可以存储任意多个值。有 5000 个同学就写成`int scores[5000];`就都能存下了。 8 | 9 | 定义一个数组只需要三个要素:变量类型、数组名、数组长度即可。 10 | 11 | ```C++ 12 | typename arrayName[arraySize]; 13 | ``` 14 | 15 | 有一点需要注意,`arrayName`的类型不是数组,而是`typename`数组。也就是说数组也是区分类型的,这也是 C++中的数组和 Python 中 List 的区别之一。 16 | 17 | ### 数组的使用 18 | 19 | #### 元素访问 20 | 21 | 对于一个数组来说,当我们需要访问其中的元素时,可以通过下标的方式来访问。 22 | 23 | 在绝大多数计算机程序当中,数组的下标通常都是从 0 开始的。第一个数存在第 0 位,第二个数存在第 1 位,以此类推。下标通过方括号表示,如: 24 | 25 | ```C++ 26 | cout << arrayName[0] << endl; 27 | ``` 28 | 29 | 注意,我们传入的下标不能大于等于数组的长度(由于是从 0 开始的),编译器往往不会报错,只会给出一个警告,但运行的过程当中可能会引发各种意想不到的问题。因为很可能你访问的内存已经超过了程序管理的范围,访问到了一些操作系统内存或者是其他禁止访问的内存,引起难以想象的后果。 30 | 31 | ```C++ 32 | int a[3]; 33 | cout << a[10] << endl; 34 | ``` 35 | 36 | 在上面的例子当中,我们声明了一个长度为 3 的数组,但是访问了下标 10。这显然超出了数组的范围,但是当我们编译的时候编译器并不会报错,只会抛出一个警告。要知道程序员往往是看不见警告的。 37 | 38 | ![](https://tva1.sinaimg.cn/large/008i3skNgy1gvm48y48tcj61h00820to02.jpg) 39 | 40 | 如果一不小心就会错过这个信息,导致潜在的风险。所以在访问之前一定要切记,确保下标在数组的范围内。 41 | 42 | #### 初始化 43 | 44 | 数组和其他变量一样,也可以在声明的时候进行初始化。 45 | 46 | 最常见的方式是将它的每一个元素的值写出来: 47 | 48 | ```C++ 49 | int a[3] = {0, 1, 2}; 50 | ``` 51 | 52 | 编译器会将花括号当中的元素一个一个地填到数组对应的位置当中,花括号当中的元素数量并不一定需要和数组长度相等,如果小于数组长度,那么就会初始化对应数量的元素。 53 | 54 | ```C++ 55 | int a[3] = {0, 1}; 56 | ``` 57 | 58 | 那么数组 a 的前两位会被初始化成 0 和 1,第三位会默认初始化为 0。 59 | 60 | 如果想要将数组当中所有元素都初始化成 0,则比较特殊,我们只需要写一个 0 即可。 61 | 62 | ```C++ 63 | int a[100] = {0}; 64 | ``` 65 | 66 | 但只有初始化成 0 的时候可以这么操作,如果传入其他值,则不会生效。 67 | 68 | 还有一种初始化方式是我们不填数组的长度,而通过初始化的方式让编译器替我们去算: 69 | 70 | ```C++ 71 | int a[] = {0, 1, 2, 3, 4}; 72 | ``` 73 | 74 | 编译器通过执行初始化知道 a 数组的长度为 5,不过 C++ primer 强烈建议我们不用这么干。因为我们人工数出来的结果可能和编译器不一样(我们会犯错),增加我们 debug 的难度。 75 | 76 | #### C++11 的初始化方式 77 | 78 | C++11 当中对于数组的初始化又有了一些新的定义,首先是可以省略等号: 79 | 80 | ```C++ 81 | int a[3] {1, 2, 3}; 82 | ``` 83 | 84 | 其次花括号内可以留空,这等价于将元素全部设置为 0: 85 | 86 | ```C++ 87 | int a[100] = {}; 88 | int b[10] {}; 89 | ``` 90 | 91 | 列表初始化时禁止缩窄转化,我们在上一篇文章当中讲过: 92 | 93 | ```C++ 94 | char cs[4] = {0, 0x3f3f3f3f, 'a', 'z'}; // 禁止,因为0x3f3f3f3f超过了char范围 95 | ``` 96 | -------------------------------------------------------------------------------- /LeetCode/01-two sum.md: -------------------------------------------------------------------------------- 1 | ## 题意 2 | 3 | 给定一个全是 int 的数组`nums`和一个整数`target`,要求返回两个下标,使得数组当中这两个下标对应的和等于 target。 4 | 5 | 你可以假设一定值存在一个答案,并且一个元素不能使用两次。 6 | 7 | 其中: 8 | 9 | - `2 <= nums.length <= 1e4` 10 | - `-1e9 <= nums[i] <= 1e9` 11 | - `-1e9 <= target <= 1e9` 12 | 13 | ## 解法一:无脑枚举 14 | 15 | 在数据里面找到两个数等于`target`,很明显可以进行枚举。 16 | 17 | 不过要使用枚举也需要解决几个问题,首先,需要枚举的数量。在这题当中,我们需要枚举任意两个数的组合,对于长度为 n 的数组来说,我们知道这样的组合一共有$C_n^2=\frac{n(n-1)}{2}$种,数量级和$n^2$相当,所以近似可以看成是$n^2$种。 18 | 19 | 对于这道题来说,数组的最大长度是`1e4`,平方之后的量级是`1e8`,差不多是 C++一秒能够执行的量级。勉勉强强可以接受大概率不会超时。 20 | 21 | 其次,是重复的情况。毕竟两两的组合这么多,如何保证得到的答案结果唯一呢?如果不唯一怎么办? 22 | 23 | 好在这道题当中替我们进行了保证,题意中明确说明了,答案唯一。既然如此,我们就可以大胆写出代码了: 24 | 25 | ```C++ 26 | class Solution { 27 | public: 28 | vector twoSum(vector& nums, int target) { 29 | int n = nums.size(); 30 | vector ret; 31 | for (int i = 0; i < n; i ++) { 32 | for (int j = i + 1; j < n; j++) { 33 | if (nums[i] + nums[j] == target) { 34 | ret.push_back(i); 35 | ret.push_back(j); 36 | return ret; 37 | } 38 | } 39 | } 40 | return ret; 41 | } 42 | }; 43 | ``` 44 | 45 | ## 解法二:使用 map 46 | 47 | 解法一的代码提交之后通过时间大约在 400 毫秒左右,仅仅超过了 6%的用户,显然还不够完美,存在大量的优化空间。 48 | 49 | 那么,我们怎么进行优化呢? 50 | 51 | 显然比较直观的就是,我们枚举了所有的可能,这太耗时了,有没有办法可以不用遍历所有的组合,但是又能保证一定可以找到答案的? 52 | 53 | 答案当然是肯定的,利用的原理也很简单,我们知道了`target`,需要在数组中寻找到 a 和 b 两个数,使得`a+b=target`。但其实我们没有必要遍历所有 a 和 b 的组合,因为确定了 a,b 也就确定了,它等于`target-a`。所以我们只需要枚举所有的 a,然后判断`target-a`元素是否存在即可。 54 | 55 | 那么问题来了,我们怎么判断`target-a`是否存在,并且找到它的位置呢? 56 | 57 | 答案很简单,使用 STL 中的 map。map 可以记录一个的 pair,我们把数组当中的数当做 key,它出现的位置当做 value。这样我们只需要提前将数组当中所有的元素都插入 map 当中,就可以了。 58 | 59 | ```C++ 60 | class Solution { 61 | public: 62 | vector twoSum(vector& nums, int target) { 63 | int n = nums.size(); 64 | vector ret; 65 | map mp; 66 | // 先把数组中元素插入map 67 | for (int i = 0; i < n; i++) { 68 | mp[nums[i]] = i; 69 | } 70 | 71 | for (int i = 0; i < n; i++) { 72 | // 枚举a,通过map判断target - a是否存在 73 | int num2 = target - nums[i]; 74 | if (mp.count(num2)) { 75 | ret.push_back(i); 76 | ret.push_back(mp[num2]); 77 | } 78 | } 79 | return ret; 80 | } 81 | }; 82 | ``` 83 | 84 | 这个代码也很简单吧,但是先别急着高兴,如果你提交的话就会发现上面的代码会有样例无法通过。为什么会无法通过呢?因为我们疏忽了一种情况,一般我们会把这种隐藏的不容易想到的情况称作“Trick”,可以看做是出题人使用的诡计。 85 | 86 | 有时候就算想到了解法,但是没有发现隐藏的 trick 也无法通过题目。所以我们不仅要想出算法,还要确保算法在所有极端情况下都能运行。 87 | 88 | 在这题当中,这个 trick 是元素的唯一性。因为我们使用了 map,map 要求所有的 key 必须唯一。如果数组当中存在重复的元素,那么后面读到的数据会覆盖前面的。覆盖会产生什么问题呢?显然会导致答案出错。 89 | 90 | 那么问题来了,在这题当中`nums`中的数唯一吗?题目中并没有说,题目中只说了确保解只有一个,但并没有说每个元素唯一。所以虽然题目没有明说,但我们依然需要对这个问题进行分析。 91 | 92 | 假设我们找到了`a+b=target`的答案,这里的 a 和 b 可能有重复吗?很明显,当 a 和 b 不相等的时候,它们都不会有重复。因为如果数组当中有两个 a,那么意味着我们至少能够找到两个 a 和 b 的组合。这与题目中说的解只有一个矛盾,所以可以肯定,当 a 和 b 不等的时候,它们都只会出现一次。 93 | 94 | 那么剩下的就是 a 和 b 相等的时候了,这也正是本题的 trick 所在。a 和 b 可能会相等,但是由于 map 中强制 key 唯一,所以只能找到一个。怎么解决呢?其实很简单,我们只需要加一个判断条件就可以解决: 95 | 96 | ```C++ 97 | 添加备注 98 | 99 | 100 | class Solution { 101 | public: 102 | vector twoSum(vector& nums, int target) { 103 | int n = nums.size(); 104 | vector ret; 105 | map mp; 106 | for (int i = 0; i < n; i++) { 107 | mp[nums[i]] = i; 108 | } 109 | 110 | for (int i = 0; i < n; i++) { 111 | int num2 = target - nums[i]; 112 | // 额外增加了mp[num2] != i的条件 113 | if (mp.count(num2) && mp[num2] != i) { 114 | ret.push_back(i); 115 | ret.push_back(mp[num2]); 116 | break; 117 | } 118 | } 119 | return ret; 120 | } 121 | }; 122 | ``` 123 | 124 | 这段代码和上面几乎完全一样,只不过底下 for 循环的 if 判断当中额外增加了 mp[num2] != i 的条件。这个条件确保我们找到了`num2`不是当前的`nums[i]`,因为当 a 和 b 相等的时候,我们想要通过 map 去寻找 b 的位置,结果由于 map 中发生了覆盖,所以又找回了 a 的位置,从而引发出错。我们加上这个判断就可以避免这种情况。 125 | 126 | 到这里还没有结束,这段代码仍然可以优化。既然 map 会发生覆盖,那么我们其实没有必要一开始的时候就一股脑把所有元素全部插入,我们可以一边插入元素一边进行判断。 127 | 128 | ```C++ 129 | class Solution { 130 | public: 131 | vector twoSum(vector& nums, int target) { 132 | int n = nums.size(); 133 | vector ret; 134 | map mp; 135 | for (int i = 0; i < n; i++) { 136 | int num2 = target - nums[i]; 137 | // 先寻找num2是否存在 138 | if (mp.count(num2)) { 139 | ret.push_back(mp[num2]); 140 | ret.push_back(i); 141 | return ret; 142 | } 143 | mp[nums[i]] = i; 144 | } 145 | 146 | return ret; 147 | } 148 | }; 149 | ``` 150 | 151 | 在这段代码当中,我们没有在一开始的时候将`nums`数组的元素全部放入 map。而是随着 for 循环逐渐插入的,在 for 循环当中,我们先去寻找了`num2`在 map 当中是否存在,再去插入的`nums[i]`。我们也没有再去判断`mp[nums2] != i`了,因为这时候`nums[i]`的值还没有插入 map,所以一定不会相等。 152 | 153 | 其实这利用了加法的交换律,`a+b=target`也可以得到`b+a=target`。a 和 b 两个数在数组当中一定有一个先后顺序,假设 a 排在 b 前面。由于我们没有事先把所有元素插入 map,所以这时候是找不到 b 的,也就是找不到答案的。但当我们遍历到 b 的时候,一定可以找到 a。并且由于我们是先判断再插入当前的元素,即使 a 和 b 相等,也不会发生覆盖。 154 | 155 | 到这里,我们就算是把这题真正做完了,不仅做出来了,而且还优化到了极致。明明是一道挺简单的题目,也没用到什么高深的算法,但是当我们深入去研究,却有这么多东西可以说。某种程度上来说,这也是算法的魅力之一。很小的细节都值得仔细研究分析,都能分析出成果来。甚至很多题的解题思路就在这些细小的分析上。 156 | 157 | 好了,关于这题就聊到这里,感谢大家的阅读。 158 | -------------------------------------------------------------------------------- /LeetCode/02-add two number.md: -------------------------------------------------------------------------------- 1 | ## 题意 2 | 3 | 题意很简单,给定两个非空的链表。用逆序的链表来表示一个整数,要求我们将这两个数相加,并且返回一个同样形式的链表。 4 | 5 | 除了数字 0 之外,这两个数都不会以 0 开头,也就是没有前导 0。 6 | 7 | 所谓的逆序的链表,题目中给了示例,也就是下面这种形式: 8 | 9 | ![](https://tva1.sinaimg.cn/large/008i3skNgy1gw2xyi3hgxj30df09i74g.jpg) 10 | 11 | 比如 2 -> 4 -> 3,存的是 342,并不是 243,这里需要注意。 12 | 13 | ### 数据范围 14 | 15 | - 每个链表中的节点数在范围 `[1, 100]` 内 16 | - `0 <= Node.val <= 9` 17 | - 题目数据保证列表表示的数字不含前导零 18 | 19 | ## 解法 20 | 21 | 题目本身很简单,困难的点在于链表的使用。 22 | 23 | 我们知道,在 C++当中有指针的概念,指针可以指向一个变量的内存地址。通过使用指针,我们可以设计一种特殊的数据结构,让某一个结构体当中存储一个指向同样类型结构体的指针。这样的话,我们就可以通过结构体当中的指针,把若干个结构体的实例连接起来,就像是一个链条一样,这种数据结构叫做链表。 24 | 25 | 我们可以来看下题目给定的链表的定义: 26 | 27 | ![](https://tva1.sinaimg.cn/large/008i3skNgy1gw2y6ek1xaj30hd05pgm0.jpg) 28 | 29 | 之前有同学问过我,你用的这个结构体哪来的,也没看到你定义。其实这是很多 OJ 的特点,我们需要实现的只是核心逻辑,有时候是写一个函数,有时候是写一个类。除了我们编写的代码之外,裁判机还会运行很多其他的逻辑。只不过这些逻辑和我们的问题没有关系,所以全部隐藏了。 30 | 31 | 比如这里链表的定义,LeetCode 已经替我们定义好了。我们只需要用就行了,至于它定义在哪里,我们并不需要关心。 32 | 33 | 我们来看下这个结构体,它的名字叫`ListNode`,顾名思义表示的是链表的节点。它当中有两个成员变量,一个是`int`型的 val,还有一个是`ListNode`指针。这里的 val 存储的是一个 0-9 的整数,表示某一个整数的其中一位,这里的指针自然是用来指向下一个节点的。 34 | 35 | 我们再来看下我们要实现的函数: 36 | 37 | ```C++ 38 | ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) 39 | ``` 40 | 41 | 传入的参数有两个,分别是两个链表的头指针,要返回的结果也是一个指针。是我们生成的新链表的头指针。 42 | 43 | 所以我们要做的事情有两个,一个是遍历`l1`和`l2`这两个链表,第二个是把其中的数相加,生成一个新的链表,返回个新的链表的头指针。 44 | 45 | 链表不像数组,我们无法知道确定的长度,只能使用`while`循环来遍历,从头结点一位一位移动,当遍历到空指针时停止。在这题里我们需要遍历两个链表,所以循环条件应该这么写: 46 | 47 | ```C++ 48 | while (l1 != nullptr || l2 != nullptr) { 49 | // todo 50 | } 51 | ``` 52 | 53 | 遍历链表之后, 剩下的就简单了,就是完成一个加法。到这个时候,我们会发现链表逆序存储是非常有好处的。因为我们拿到的每一个元素它的位置是确定的,第一个拿到的一定是个位,第二个拿到的是十位。如果我们链表是正序存储,我们在知道链表长度之前,是不知道第一位到底是哪一位的。 54 | 55 | 所以表面上看逆序存储的链表好像有点麻烦,其实是替我们简化了问题。 56 | 57 | 最后,我们只需要注意一下加法的进位, 很容易写出代码: 58 | 59 | ```C++ 60 | class Solution { 61 | public: 62 | ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) { 63 | ListNode* ret = new ListNode(); 64 | ListNode* pnt = ret; 65 | bool carry = false; 66 | while (l1 != nullptr || l2 != nullptr || carry) { 67 | int cur = 0; 68 | if (l1 != nullptr) { 69 | cur += l1->val; 70 | l1 = l1->next; 71 | } 72 | if (l2 != nullptr) { 73 | cur += l2->val; 74 | l2 = l2->next; 75 | } 76 | if (carry) { 77 | cur ++; 78 | } 79 | if (cur >= 10) { 80 | cur -= 10; 81 | carry = true; 82 | }else { 83 | carry = false; 84 | } 85 | pnt->next = new ListNode(cur); 86 | pnt = pnt->next; 87 | } 88 | return ret->next; 89 | } 90 | }; 91 | ``` 92 | 93 | 关于进位的处理没什么好说的,唯一要注意一下的就是`while`循环当中的条件,多了一个是否进位的判断。这是对最高位发生进位时的处理,否则会导致最高位的进位丢失。 94 | 95 | 只要注意到这个 trick,并且了解链表基本的使用,这道题也就迎刃而解了,是不是很简单呢? 96 | -------------------------------------------------------------------------------- /LeetCode/04-median of two sorted arrays.md: -------------------------------------------------------------------------------- 1 | ## 题面 2 | 3 | 4 | 这题的题面非常简单,简单到只有一句话:要求两个有序数组的中位数。 5 | 6 | 7 | 8 | 我们来看两个示例: 9 | 10 | 11 | 12 | > 示例 1: 13 | > 14 | > 输入:nums1 = [1,3], nums2 = [2] 15 | > 输出:2.00000 16 | > 解释:合并数组 = [1,2,3] ,中位数 2 17 | > 示例 2: 18 | > 19 | > 输入:nums1 = [1,2], nums2 = [3,4] 20 | > 输出:2.50000 21 | > 解释:合并数组 = [1,2,3,4] ,中位数 (2 + 3) / 2 = 2.5 22 | 23 | 24 | 25 | ## 分析 26 | 27 | 28 | 29 | 首先,我们很容易可以想到最简单的解法, 就是可以先把两个数组归并,归并成一个数组之后,中位数也就水到渠成了。 30 | 31 | 32 | 33 | 由于两个数组天然有序,我们在归并的时候都不需要排序,直接采用类似归并排序的操作,可以在$O(n)$的复杂度内完成。这样的代码写起来也非常简单,只有几行: 34 | 35 | 36 | 37 | ```C++ 38 | double findMedianSortedArrays(vector& nums1, vector& nums2) { 39 | int n = nums1.size(), m = nums2.size(); 40 | // 往两个数组之后插入最大值,用来在归并时防止下标越界 41 | nums1.push_back(INT_MAX); 42 | nums2.push_back(INT_MAX); 43 | vector merge; 44 | int i1 = 0, i2 = 0; 45 | // 归并两个数组 46 | for (int i = 0; i < n+m; i++) { 47 | if (nums1[i1] <= nums2[i2]) { 48 | merge.push_back(nums1[i1++]); 49 | }else merge.push_back(nums2[i2++]); 50 | } 51 | n += m; 52 | if (n % 2) return merge[n / 2]; 53 | else return (merge[n / 2] + merge[n / 2 - 1]) / 2.0; 54 | } 55 | ``` 56 | 57 | 58 | 59 | 这个算法虽然简单,但一样能够通过,耗时也仅有24ms。 60 | 61 | 62 | 63 | 如果仅仅是以通过这题为目标的话,那到这里就结束了。但如果我们仔细分析,会发现这样的操作有一个巨大的问题,就是我们额外申请了一个内存,用来存储了两个数组归并之后的结果。 64 | 65 | 66 | 67 | 在工程实现当中,这样的做法往往是比较忌讳的,因为内存是很宝贵的,最好不要在函数内部随意开辟大块的内存,往往会影响程序的性能。 68 | 69 | 70 | 71 | 另外,在题目描述当中也明确说了,这道题算法的时间复杂度应该为 `O(log (m+n))` 。如果在面试当中遇到,那么这样的解法才是正解。 72 | 73 | 74 | 75 | ## 思考 76 | 77 | 78 | 79 | 题目当中限制了复杂度,这既是一种限制,其实反过来想也未尝不是一种提示。 80 | 81 | 82 | 83 | 通过对问题复杂度估计来反推或者筛选算法,是算法竞赛当中常见的技巧之一。$\log N$ 级别的算法非常少,因此基本上到这里就可以锁定,这题肯定需要使用二分法。 84 | 85 | 86 | 87 | 虽然我们明确了应该使用二分法,但是我们距离解出问题还有很多,仍然有许多问题需要解决。我们一个一个来看。 88 | 89 | 90 | 91 | 首先一个问题是,由于我们有两个数组,我们怎么确定答案在哪个数组当中呢? 92 | 93 | 94 | 95 | 这个问题并不难,因为只有两个数组A和B,我们完全可以进行穷举,先假设在A数组当中进行寻找,如果找不到那自然答案在B数组。真正的问题是我们如何确定某个值是不是答案呢?换句话说,答案应该满足什么条件呢? 96 | 97 | 98 | 99 | 对于这个问题我们可以从题目本身入手,既然是要求中位数,那么这个值自然应该满足中位数的要求。即,在两个数组合并的所有元素当中,排序位于正中。 100 | 101 | 102 | 103 | 假设A数组有n个元素,B数组有m个元素,对于某个值x,它在A数组中排名a,在B数组中排名是b。如果x是答案,那么要满足$a + b = \frac {n+m} 2 $。 104 | 105 | 106 | 107 | 假设我们在A数组当中使用二分法寻找答案,那么对于A数组当中的每个元素,我们都能获得它的下标,它的下标自然就是它在A数组中的排名。所以我们只需要求到它在B数组中的排名,然后判断是否满足中位数的条件即可。 108 | 109 | 110 | 111 | 到这里,问题就转化成了,我们怎么样求A数组中的元素在B数组中的排名呢? 112 | 113 | 114 | 115 | 当然,我们可以想到可以使用二分法,我们在B数组当中做一下二分,不就行了吗? 116 | 117 | 118 | 119 | 实际上我们再深入分析一下会发现,其实并没有必要。因为答案要满足$a + b = \frac{n+m}2$,我们已经知道了a,其实并不一定要求b,我们可以反过来思考,验证一下$b = \frac{n+m} 2 - a$是否成立即可。 120 | 121 | 122 | 123 | 假设我们要验证`A[k]`元素是否是答案,如果它是答案,那么它在B数组中的排名应该是$\frac{n+m}2 - k-1$,那么我们把它和B数组中对应位置的元素进行一下比较。为了书写方便,我们把$\frac{n+m}2 - k-1$写成`k2`,如果`B[k2] <= A[k] <= B[k2+1]`,说明`A[k]`在B数组的排名就是我们想要的。如果`A[k] < B[k2]`,说明我们的k取小了,如果`A[k] > B[k2+1]`,则说明我们的k取大了。 124 | 125 | 126 | 127 | 也就是说我们还是只在A数组当中进行二分,只不过使用了B数组来作为我们二分判断的条件。 128 | 129 | 130 | 131 | 但这里只考虑了答案在A数组中的情况,如果答案在B数组里呢?我们当然可以再对B数组进行一次同样的处理,但其实没有必要,我们在考虑`A[k]`是答案的同时也可以考虑一下`B[k2]`是答案的可能性。这样,我们就只需要一次操作就可以找到答案了。 132 | 133 | 134 | 135 | LeetCode官方提供了另外一种思路,可能更好理解一些。 136 | 137 | 138 | 139 | 我们可以将A数组和B数组都分成两个部分,分界线分别是k1, k2。 140 | 141 | 142 | 143 | ``` 144 | A[0], A[1],...A[k1], ... A[n-1] 145 | B[0], B[1],...B[k2], ... B[m-1] 146 | ``` 147 | 148 | 149 | 150 | 其中k1是A数组的分割点,k2是B数组的分割点,满足$k1 + k2 = \frac{n+m} 2$以及`A[k1] <= B[k2+1] && B[k2] <= A[k1+1]`。 151 | 152 | 153 | 154 | 我们仔细分析一下,要同时满足`A[k1] <= B[k2+1] && B[k2] <= A[k1+1]`这两个条件,本质上就是寻找满足`A[k1] <= B[k2+1]`的最大的k1。因为只有k1最大时,才能保证`B[k2] <= A[k1+1]`。 155 | 156 | 157 | 158 | 要找满足条件的最大k1,我们同样可以使用二分法来逼近答案。找到了k1之后,k2也就出来了,整理一下就能获得答案。 159 | 160 | 161 | 162 | 如果n+m为奇数,那么min(A[k1+1], B[k2+1])就是答案,如果为偶数,那么答案是$\frac{max(A[k1], B[k2]) + min(A[k1+1], B[k2+1])}{2}$。 163 | 164 | 165 | 166 | 这两个思路本质上是一样的,但我个人感觉官方题解里的思路更容易理解一些。 167 | 168 | 169 | 170 | 最后我们来看下代码,这段代码是我写过好几版之后最精简的一版,当中的边界情况非常地多,大家在看代码的时候最好留意一下。 171 | 172 | 173 | 174 | ```C++ 175 | class Solution { 176 | public: 177 | 178 | pair getValue(vector& nums1, vector& nums2, int pos) { 179 | // [0, n+1) n = nums1.size() 180 | int l = 0, r = nums1.size()+1; 181 | double med1 = 0, med2 = 0; 182 | while (l < r) { 183 | int m = (l + r) >> 1; 184 | // vi1 = A[k1],注意k1小于0的情况 185 | int vi1 = (m == 0 ? INT_MIN : nums1[m-1]); 186 | // vi2 = A[k1+1],注意k1+1超界的情况 187 | int vi2 = (m == nums1.size() ? INT_MAX : nums1[m]); 188 | // vj1 = B[k2],注意k2小于0的情况 189 | int vj1 = (m == pos ? INT_MIN : nums2[pos-m-1]); 190 | // vj2 = B[k2+1],注意k2+1超界的情况 191 | int vj2 = (pos-m == nums2.size() ? INT_MAX : nums2[pos-m]); 192 | 193 | if (vi1 <= vj2) { 194 | med1 = max(vi1, vj1); 195 | med2 = min(vi2, vj2); 196 | l = m + 1; 197 | }else { 198 | r = m; 199 | } 200 | } 201 | return make_pair(med1, med2); 202 | } 203 | 204 | double findMedianSortedArrays(vector& nums1, vector& nums2) { 205 | if (nums1.size() > nums2.size()) { 206 | return findMedianSortedArrays(nums2, nums1); 207 | } 208 | int n = nums1.size() + nums2.size(); 209 | if (n % 2) { 210 | return getValue(nums1, nums2, n >> 1).second; 211 | }else { 212 | auto p = getValue(nums1, nums2, n >> 1); 213 | return (p.first + p.second) / 2.0; 214 | } 215 | } 216 | }; 217 | ``` 218 | 219 | 220 | 221 | ## -------------------------------------------------------------------------------- /LeetCode/05-longest palindromic substring.md: -------------------------------------------------------------------------------- 1 | ## 题意 2 | 3 | 题目非常简单只有一句话:给你一个字符串 `s`,找到 `s` 中最长的回文子串。 4 | 5 | 6 | 7 | ![](https://tva1.sinaimg.cn/large/008i3skNgy1gz9aehwxf4j30xy0k4t9k.jpg) 8 | 9 | 10 | 11 | 这题的暴力解法很容易想到,我们只需要枚举一下回文中心的位置,然后针对每一个回文中心去找它的最长回文子串即可。 12 | 13 | 14 | 15 | 不过有一点需要注意,回文串有两种一种是奇回文,一种是偶回文。顾名思义,如果是奇回文,那么回文串的长度是奇数。如果是偶回文,自然就是偶数。这两个在枚举的时候是不一样的,需要注意。 16 | 17 | 18 | 19 | 暴力求解的算法很容易写: 20 | 21 | ```C++ 22 | class Solution { 23 | public: 24 | string longestPalindrome(string s) { 25 | string ret; 26 | for (int i = 0; i < s.size(); i++) { 27 | int l = i, r = i; 28 | // 奇回文,回文中心是i 29 | while (l >= 0 && r < s.size() && s[l] == s[r]) { 30 | l--; 31 | r++; 32 | } 33 | if (r - l - 1 > ret.size()) { 34 | ret = s.substr(l+1, r-l-1); 35 | } 36 | l = i-1; 37 | r = i; 38 | // 偶回文,回文中心是i和i-1 39 | while (l >= 0 && r < s.size() && s[l] == s[r]) { 40 | l --; 41 | r ++; 42 | } 43 | if (r - l - 1 > ret.size()) { 44 | ret = s.substr(l+1, r-l-1); 45 | } 46 | } 47 | return ret; 48 | } 49 | }; 50 | ``` 51 | 52 | 53 | 54 | 我们简单分析一下复杂度,极端情况下,比如整个字符串的所有字符都相等时,那么每次枚举回文串都会将整个字符串遍历一遍。很明显,在这种情况下的复杂度就是$O(n^2)$。 55 | 56 | 57 | 58 | 虽然复杂度看起来有些高,但一样可以通过,因为题目当中明确写了,字符串的长度最长只有1000. 59 | 60 | 61 | 62 | 那么还有没有更快的算法呢? 63 | 64 | 65 | 66 | 当然是有的,这就要介绍到今天的主角,manacher算法,俗称马拉车算法。 67 | 68 | 69 | 70 | 马拉车算法的核心原理是利用之前已经找到的回文子串的性质,来快速求解之后的回文子串的长度。怎么利用呢?我们来看一张图,为了方便起见,我们将字符串画成一条线。 71 | 72 | 73 | 74 | ![](https://tva1.sinaimg.cn/large/008i3skNgy1gz9aw7gyxnj31lg09cmxc.jpg) 75 | 76 | 77 | 78 | 我们假设它当中的某一个位置i能够找到的回文子串的左右端点分别是left和right,那么i的回文半径就是right-i。 79 | 80 | 81 | 82 | 这个时候,假设我们要求i的右侧的某一点j的回文长度,我们可以怎么求呢? 83 | 84 | 85 | 86 | 首先我们可以找到j关于i的对称位置j',因为j'一定在i的左侧,所以如果我们是按照从左往右的顺序来求每一点对称半径的话,j'的对称半径是已知的。 87 | 88 | 89 | 90 | ![](https://tva1.sinaimg.cn/large/008i3skNgy1gz9b16bck9j31hk08sglt.jpg) 91 | 92 | 93 | 94 | 这个时候根据j'对称半径的长度,有三种可能:第一种,这个对称半径比较小。我们和什么比呢?我们和left的位置比,也就是说这个对称范围的左侧在left的右边,如图: 95 | 96 | 97 | 98 | ![](https://tva1.sinaimg.cn/large/008i3skNgy1gz9b5aa4uuj31ge09kt8y.jpg) 99 | 100 | 101 | 102 | 假设我们用一个数组`radis`存储了之前所有位置的对称半径,那么由于j和j’关于i对称,那么根据对称性,我们可以知道`radis[j] = radis[j']`。 103 | 104 | 105 | 106 | 再来看第二种可能,第二种可能就是j'的对称半径比较大,它的范围左侧超过了left: 107 | 108 | 109 | 110 | ![](https://tva1.sinaimg.cn/large/008i3skNgy1gz9b74mrmhj31gu09idg3.jpg) 111 | 112 | 113 | 114 | 在这种情况下,j的对称半径还能取到`radis[j'`]吗? 115 | 116 | 117 | 118 | 答案是取不到,因为如果j的对称范围也有这么大的话,下图圈起来的两个部分就要对称了。 119 | 120 | 121 | 122 | ![](https://tva1.sinaimg.cn/large/008i3skNgy1gz9b97822xj31ew0b6gm4.jpg) 123 | 124 | 125 | 126 | 实际上它们是不可能对称的,因为i的对称半径已经确定了,如果它们还能对称,那么就和i的对称半径矛盾了。所以在这种情况下,j的对称半径就是`right-j`。 127 | 128 | 129 | 130 | 我们再来看最后一种情况,就是j'的对称半径的左侧刚好和left重合,在这种情况下,j的对称范围的右侧也会刚好和right重合。 131 | 132 | 133 | 134 | ![](https://tva1.sinaimg.cn/large/008i3skNgy1gz9bcdgxgrj31hs09oaac.jpg) 135 | 136 | 137 | 138 | 我们来思考一个问题,这个时候j的回文半径确定了吗? 139 | 140 | 141 | 142 | 答案是没有,它还可以继续往右侧延伸,因为j'的对称范围到left就停住了,right的右侧再构成新的对称也不会构成矛盾。所以这种情况下,我们还是需要用一层循环去拓展j的范围。 143 | 144 | 145 | 146 | 现在我们厘清了所有的情况,要怎么求最长的回文子串呢?很简单,我们从左往右遍历,每次维护最右侧的位置right以及它对应的回文中心i。 147 | 148 | 149 | 150 | 到这里还剩下一个问题:回文分为奇回文和偶回文,上面的算法只能解决奇回文的情况,对于偶回文怎么办呢? 151 | 152 | 153 | 154 | 这个问题很好回答,我们可以在算法开始之前先对字符串做一个预处理,把所有偶回文的情况也转换成奇回文。 155 | 156 | 157 | 158 | 比如: 159 | 160 | 161 | 162 | abba -> #a#b#b#a# 163 | 164 | 165 | 166 | 这样一来,回文中心就变成中间的#了。我们再来看原本是奇回文的情况: 167 | 168 | 169 | 170 | aba -> #a#b#a# 171 | 172 | 173 | 174 | 回文中心还是在b上,依然还是奇回文。 175 | 176 | 177 | 178 | 预处理的代码: 179 | 180 | 181 | 182 | ```C++ 183 | string transform(string& str) { 184 | string ret; 185 | for (int i = 0; i < str.length(); i++) ret = ret + '#' + str[i]; 186 | return '$' + ret + '#'; 187 | } 188 | ``` 189 | 190 | 191 | 192 | 我们来看完整代码: 193 | 194 | 195 | 196 | ```C++ 197 | class Solution { 198 | public: 199 | 200 | // 字符串预处理 201 | string transform(string& str) { 202 | string ret; 203 | for (int i = 0; i < str.length(); i++) ret = ret + '#' + str[i]; 204 | return '$' + ret + '#'; 205 | } 206 | 207 | string longestPalindrome(string s) { 208 | string st = transform(s); 209 | int n = st.length(); 210 | // 存储每一个位置的回文半径 211 | int arr[n+2]; 212 | // mr 是对称位置的最右侧,idx为对应位置的回文中心 213 | // max_radis是当前最长回文半径,max_id最长回文半径的对称中心 214 | int mr = 0, idx = 0, max_radis = 0, max_id = 0; 215 | for (int i = 1; i < n; i++) { 216 | // 如果mr在i右侧,利用回文性质快速求arr[i] 217 | arr[i] = mr > i ? min(arr[2*idx - i], mr-i) : 1; 218 | // 如果i在mr右侧,或者回文范围刚好覆盖的情况 219 | if (i >= mr || i + arr[i] == mr) { 220 | while (st[i - arr[i]] == st[i + arr[i]]) { 221 | if (arr[i] > max_radis) { 222 | max_radis = arr[i]; 223 | max_id = i; 224 | } 225 | arr[i]++; 226 | if (i + arr[i] > mr) { 227 | mr = i + arr[i]; 228 | idx = i; 229 | } 230 | } 231 | } 232 | } 233 | 234 | // 还原回文串 235 | string ret = ""; 236 | for (int i = max_id - max_radis; i < max_id + max_radis; i++) { 237 | if (st[i] != '#') ret = ret + st[i]; 238 | } 239 | return ret; 240 | } 241 | }; 242 | ``` 243 | 244 | 245 | 246 | 最后我们来思考一个问题,为什么马拉车算法的复杂度是$O(n)$呢? 247 | 248 | 249 | 250 | 原因其实很简单,虽然我们的代码当中有两层循环,但是我们仔细观察一下会发现,不论循环怎么执行,mr这个变量是一直递增的。mr最多只能从0递增到n,所以这是一个$O(n)$的算法。 251 | 252 | 253 | 254 | 马拉车算法巧妙地利用了对称性简化了计算过程,这一思想在很多其他字符串处理算法上 非常常见。因此,仔细了解它的原理非常有必要。 -------------------------------------------------------------------------------- /LeetCode/10-regular expression matching.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## 题意 4 | 5 | 6 | 7 | 给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 '.' 和 '*' 的正则表达式匹配。 8 | 9 | - '.' 匹配任意单个字符 10 | - '*' 匹配零个或多个前面的那一个元素 11 | 12 | 13 | 14 | 所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。 15 | 16 | 17 | 18 | ![](https://tva1.sinaimg.cn/large/e6c9d24egy1gzikshq5m0j21b60u00vi.jpg) 19 | 20 | 21 | 22 | ## 解法 23 | 24 | 25 | 26 | 首先还是老规矩,我们拿到题目审完题之后先看数据范围。数据范围是很强的提示信息,既体现了题目的难度,有的时候也能解读出很多信息。 27 | 28 | 29 | 30 | 本题的范围很小,两个串最大长度才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 | 由于我们判断匹配是在两个字符串s和p中间的,所以每一种匹配的情况都会对应s和p中的两个位置。我们可以用一个pair对来记录匹配的状态,表示字符串s[0:i]的子串和字符串p[0:j]的子串匹配。这样我们只需要维护所有能够匹配上的,如果能够匹配到两个字符串的末尾,也就是说出现在合法的状态当中,就说明有解,这里的n表示s的长度,m表示p串的长度。 55 | 56 | 57 | 58 | 这种用两个数字表示状态的方法是字符串匹配问题当中的常用技巧,我们可以顺着这个思路,要做的就是使用一个数据结构维护所有合法(匹配)的状态,通过当前的合法状态寻找新的合法状态,只要能找到最终要求的状态,那么就说明有解。如果找遍了所有合法的状态也没有找到最终状态,就说明无解。 59 | 60 | 61 | 62 | 如果大家学过宽度优先搜索,那么可以很自然地想到可以使用队列来记录所有的状态。那么顺水推舟,不难写出代码。 63 | 64 | 65 | 66 | 在这段代码当中我们使用了一个小技巧,我们将字符串s和p向右移动了一位,有效字符从下标1开始。这是因为我们用下标0表示空串的状态,如果下标从0开始,则空串的表达不太方便,代码书写会比较繁琐。 67 | 68 | 69 | 70 | 完整代码如下: 71 | 72 | 73 | 74 | ```C++ 75 | class Solution { 76 | public: 77 | bool isMatch(string s, string p) { 78 | int n = s.size(), m = p.size(); 79 | s = ' ' + s; 80 | p = ' ' + p; 81 | queue> que; 82 | // s和p都是空串可以匹配上,所以是合法的 83 | que.push(make_pair(0, 0)); 84 | 85 | while(!que.empty()) { 86 | pair head = que.front(); 87 | que.pop(); 88 | int i = head.first, j = head.second; 89 | // 如果s和全部和p串全部都已经完成了匹配,返回true 90 | if (i == n && j == m) { 91 | return true; 92 | } 93 | // 跳过非法长度 94 | if (i > n || j > m) continue; 95 | // 如果s[i+1]和p[j+1]匹配 96 | if (i < n && j < m && (s[i+1]==p[j+1] || p[j+1] == '.')) { 97 | que.push(make_pair(i+1, j+1)); 98 | } 99 | // 如果p[j+2]=*,需要考虑p[j+1]位置出现0次即被删除的情况 100 | if (j+2 <= m &&p[j+2] == '*') { 101 | que.push(make_pair(i, j+2)); 102 | } 103 | if (p[j] == '*') { 104 | // 如果p[j]=*,s[i+1]能够和p[j-1]匹配,那么也是合法状态 105 | if (i < n && (j > 0 && s[i+1] == p[j-1] || p[j-1] == '.')) { 106 | que.push(make_pair(i+1, j)); 107 | } 108 | } 109 | } 110 | return false; 111 | } 112 | }; 113 | ``` 114 | 115 | 116 | 117 | 写完了代码会发现这其实就是一个典型的宽度优先搜索问题,我们搜索了所有合法的状态,如果最终状态被搜索到,那么说明有解,如果没有,说明无解。 118 | 119 | 120 | 121 | 这段代码虽然能够AC,但是仔细分析会发现一个问题。问题在于我们枚举状态的时候会有重复,一个中间状态可能会反复被枚举到,从而出现很多次。比如<2, 2>可以推导到<3, 3>,但<2, 3>也可能推导到<3, 3>,<3, 1>也可能推导到<3, 3>。极端情况下,状态会有很多的重复,从而导致性能损耗。 122 | 123 | 124 | 125 | 对于这个问题,其实很好解决,我们有一个非常简单的办法可以解决这个问题。就是把所有出现过的状态记录下来,每次有新的状态时就去检查一下是否已经被记录了,从而之前已经记录过的状态。 126 | 127 | 128 | 129 | 在C++当中我们可以使用set或者是map做到这一点,代码和刚才相差不大,只不过在往队列插入状态之前,增加了一个判断是否出现的逻辑。这种搜索结合记忆的做法就叫做记忆化搜索,看起来术语很高大上,但其实逻辑并不难。 130 | 131 | 132 | 133 | ```C++ 134 | class Solution { 135 | public: 136 | bool isMatch(string s, string p) { 137 | int n = s.size(), m = p.size(); 138 | s = ' ' + s; 139 | p = ' ' + p; 140 | queue> que; 141 | que.push(make_pair(0, 0)); 142 | 143 | set> st; 144 | 145 | while(!que.empty()) { 146 | pair head = que.front(); 147 | que.pop(); 148 | int i = head.first, j = head.second; 149 | if (i == n && j == m) { 150 | return true; 151 | } 152 | if (i > n || j > m) continue; 153 | pair cur; 154 | if (i < n && j < m && (s[i+1] == p[j+1] || p[j+1] == '.')) { 155 | cur = make_pair(i+1, j+1); 156 | // 如果没有出现在st里才会被添加 157 | if (st.count(cur) == 0) { 158 | que.push(cur); 159 | st.insert(cur); 160 | } 161 | } 162 | if (j+2 <= m && p[j+2] == '*') { 163 | cur = make_pair(i, j+2); 164 | if (st.count(cur) == 0) { 165 | que.push(cur); 166 | st.insert(cur); 167 | } 168 | } 169 | if (p[j] == '*') { 170 | if (i < n && (j > 0 && s[i+1] == p[j-1] || p[j-1] == '.')) { 171 | cur = make_pair(i+1, j); 172 | if (st.count(cur) > 0) continue; 173 | que.push(make_pair(i+1, j)); 174 | st.insert(cur); 175 | } 176 | } 177 | } 178 | return false; 179 | } 180 | }; 181 | ``` 182 | 183 | 184 | 185 | ### 逆向推导 186 | 187 | 188 | 189 | 看完了正推的解法, 我们再来想想逆推吧。 190 | 191 | 192 | 193 | 这道题逆推的思路还是比较明显的,在正推中,我们已知合法,探索, , 合法的可能性。逆推就是反过来,我们要求,探索它成立时需要满足的条件。比如,假设合法且s[i]和p[j]匹配,那么显然也合法。但由于我们是逆推的,所以我们没办法直接知道是否合法,所以我们要继续推导下去。 194 | 195 | 196 | 197 | 说到这里,估计有些小伙伴已经反应过来了,这样的推导方式不就是递归吗? 198 | 199 | 200 | 201 | 其实整个的思路和代码的写法和上面的做法几乎是一样的,不过一个是从合法的<0, 0>开始,一个是从要求的开始而已。 202 | 203 | 204 | 205 | ```C++ 206 | class Solution { 207 | public: 208 | // 这里要注意,字符串传参会有开销,因此要用引用 209 | bool dfs(string &s, string &p, int n, int m) { 210 | // <0, 0>是合法状态,因此return true 211 | if (n == 0 && m == 0) return true; 212 | if (n < 0 || m < 0) return false; 213 | // 如果s[n]=p[m]递归求 214 | if (n > 0 && (s[n] == p[m] || p[m] == '.')) { 215 | return dfs(s, p, n-1, m-1); 216 | } 217 | // p[m]=*的情况 218 | if (m > 1 && p[m] == '*') { 219 | bool match = p[m-1] == s[n] || p[m-1] == '.'; 220 | return dfs(s, p, n, m-2) || (match && dfs(s, p, n-1, m)); 221 | } 222 | return false; 223 | } 224 | 225 | bool isMatch(string s, string p) { 226 | int n = s.size(), m = p.size(); 227 | s = ' ' + s; 228 | p = ' ' + p; 229 | return dfs(s, p, n, m); 230 | } 231 | }; 232 | ``` 233 | 234 | 235 | 236 | ### 动态规划 237 | 238 | 239 | 240 | 我们搞定了逆向推导之后,其实还没有结束,在递归的做法当中,我们求每一个状态都是通过递归的方式去获取的。除了递归之外,还有没有其他办法? 241 | 242 | 243 | 244 | 其实是有的,也不难想到,我们完全可以通过一个二维数组来记录。`dp[i][j]`表示s[0:i]和p[0:j]能否构成匹配,由于我们已知`dp[0][0]=true`,所以我们可以按照顺序遍历状态进行推导即可。从状态的枚举上,我们是正推的思路,从最初合法的状态<0, 0>向外枚举,而对于每一个要求的状态而言,我们是通过逆推的方式去计算它的结果。 245 | 246 | 247 | 248 | 为什么大家都说动态规划的算法很难,难点其实就在这里,它是搜索算法的集大成者,也是正反两种思路的集成,因此对于思维的要求很高。但往往编码都比较简单,想出算法之后很容易实现。 249 | 250 | 251 | 252 | 我们来看代码: 253 | 254 | 255 | 256 | ```C++ 257 | bool isMatch(string s, string p) { 258 | int n = s.size(), m = p.size(); 259 | s = ' ' + s; 260 | p = ' ' + p; 261 | bool dp[n+5][m+5]; 262 | memset(dp, 0, sizeof dp); 263 | dp[0][0] = true; 264 | for (int i = 0; i <= n; i++) { 265 | for (int j = 1; j <=m; j++) { 266 | // 判断s[i]和p[j]是否匹配 267 | bool matched = s[i] == p[j] || p[j] == '.'; 268 | if (matched && i > 0) dp[i][j] = dp[i-1][j-1]; 269 | // 判断p[j]是否为* 270 | if (j > 1 && p[j] == '*') { 271 | // 判断p[j-1]是否和s[i]匹配 272 | matched = p[j-1] == s[i] || p[j-1] == '.'; 273 | // dp[i][j-2]表示p[j-1]出现0次,dp[i-1][j]表示s[i]通过*重复完成匹配 274 | dp[i][j] = dp[i][j-2] || (i > 0 && matched && dp[i-1][j]); 275 | } 276 | } 277 | } 278 | return dp[n][m]; 279 | } 280 | ``` 281 | 282 | -------------------------------------------------------------------------------- /LeetCode/11-container with most water.md: -------------------------------------------------------------------------------- 1 | ## 题意 2 | 3 | 4 | 5 | 给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。 6 | 7 | 找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。 8 | 9 | 返回容器可以储存的最大水量。 10 | 11 | 说明:你不能倾斜容器。 12 | 13 | 14 | 15 | ![](https://tva1.sinaimg.cn/large/e6c9d24egy1gzoregygrdj20x70u0dhl.jpg) 16 | 17 | 18 | 19 | ## 解法 20 | 21 | 22 | 23 | 还是老规矩,我们先来看下数据范围。这里n的最大范围是1e5,那么很明显,我们基本上可以排除掉$O(n\log n)$复杂度以上的算法了。至少暴力枚举估计是不行了。 24 | 25 | 26 | 27 | 看完数据范围之后我们来分析题目,题面很简单,就是要求一个最大的横截面积。我们来分析一下这个面积的组成,很容易发现,它一定是一个矩形。它的长是两个挡板之间的距离,宽是两个挡板当中短的那个长度。我们要做的就是找到这样的挡板,使得它们围成的矩形面积最大。 28 | 29 | 30 | 31 | 如果我们直接苦思冥想去思考怎么样找到这样的挡板对,可能会比较复杂,有一种无从下手的感觉。因为我们前面已经排除掉了暴力的方法,直接枚举一定会超时。这个时候就需要我们先停下来,思考一下曲线救国的途径了。 32 | 33 | 34 | 35 | 想要曲线救国还是要先回到题目当中来,先来分析一下题意。我们都知道矩形的面积取决于长和宽,如果长和宽都增大,那么面积一定增大。如果长和宽一个增大一个减小,矩形的面积有可能增大也有可能减小。在本题当中,矩形的宽最大是已知的,就是n-1。对于宽最大的情况来说,矩形的长也是已知的,就是`max(height[0], height[n-1])`。 36 | 37 | 38 | 39 | 无论我们再怎么寻找,我们都不可能找到宽更长的矩形,所以要使得矩形面积更大的话,必须要有更长的长边。而长边取决于两个挡板当中较短的一个,所以我们很自然可以想到,对于一个已知的矩形,我们想要寻找更大面积的可能,只需要固定其较长的边,对于较短的边则往内部遍历寻找更长的边。如果能够找到更长的边,就说明矩形的面积存在增大的可能。 40 | 41 | 42 | 43 | 我们依次更新两边的隔板,维护最大面积即可。 44 | 45 | 46 | 47 | ```C++ 48 | class Solution { 49 | public: 50 | int maxArea(vector& height) { 51 | // 左侧挡板从0开始,右侧挡板设置成n-1 52 | int l = 0, r = height.size()-1; 53 | int ret = min(height[l], height[r]) * (r-l); 54 | while (l < r) { 55 | // 如果左侧挡板更小 56 | if (height[l] < height[r]) { 57 | // 则左侧挡板往内移动 58 | while (l < r && height[l] < height[r]) { 59 | l++; 60 | ret = max(ret, min(height[l], height[r]) * (r-l)); 61 | } 62 | // 如果右侧挡板较小 63 | }else { 64 | while (l < r && height[l] >= height[r]) { 65 | r--; 66 | ret = max(ret, min(height[l], height[r]) * (r-l)); 67 | } 68 | } 69 | } 70 | return ret; 71 | } 72 | }; 73 | ``` 74 | 75 | -------------------------------------------------------------------------------- /LeetCode/15-3sum.md: -------------------------------------------------------------------------------- 1 | ## 题意 2 | 3 | 4 | 5 | 给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组。 6 | 7 | 注意:答案中不可以包含重复的三元组。 8 | 9 | 10 | 11 | ![](https://tva1.sinaimg.cn/large/e6c9d24egy1gzyxl6xsffj21500oqgmq.jpg) 12 | 13 | 14 | 15 | ## 解法 16 | 17 | 18 | 19 | 借着这道题,我们再重温一下拿到题目之后的思考过程。首先还是老规矩,看完题意之后首先来看看数据范围。 20 | 21 | 22 | 23 | 数据范围当中隐藏的信息很多,还可以用来大致确定一下正确解答的复杂度,有很强的提示性。所以千万不要忽略,一定要仔细查看。 24 | 25 | 26 | 27 | 这道题当中数据的范围并不大,最多只有3000个数,每个数的范围是1e5,即使是三个数相加也不会超过int的范围,相对比较安全。其次最多只有3000个数,意味着$O(n^2)$甚至是$O(n^2\log n)$都是可以接受的,相对来说,复杂度卡的不是非常死。 28 | 29 | 30 | 31 | 分析完了复杂度之后,我们可以尝试一下暴力的方法,看看暴力求解有没有什么问题,有哪些问题。绝大多数的问题暴力求解是不行的,但这并不是白费功夫。思考完了暴力解法之后,我们就能找到题目的难点。 32 | 33 | 34 | 35 | 也就是说第二个步骤是找到难点,几乎所有的算法题都是有难点的,这些难点基本上都是出题人刻意设置的。所以当我们不能或者是很难直接求解找到突破口的时候,不妨尝试一下难点分析,即想一想这题究竟难在哪里,真正困难的点在哪里。 36 | 37 | 38 | 39 | 以本题为例,我们要找到三个数,使得它们的和为0,并且要找出所有的组合。对于找所有解的问题来说,它的前提就是我们要能枚举所有可能构成解的可能性。在这道题当中,三个数的组合数量是$O(n^3)$的量级,这是无法接受的。 40 | 41 | 42 | 43 | 那么难点也就找到了:我们要枚举的可能性太多,会导致复杂度无法接受。所以我们要做的就是想办法既能够枚举所有的可能性,又保证复杂度不会超过限制。 44 | 45 | 46 | 47 | 从理论上看,n个数当中找3个,无论如何也有$n^3$的量级,看似是无解的。但是我们仔细分析题目,可以找到突破口,这个突破口就是三个数和为0。既然三个数和为0,那么就对这三个数的组成有了一定的限制。很明显,这三个数不能全大于0,也不能全小于0。除非全为0,否则必须有正有负。进而我们可以想到,我们可以枚举其中一个值,在此基础上寻找另外两个值。 48 | 49 | 50 | 51 | 假设a+b+c=0,我们不妨设$a \le b \le c$。那么我们只需要枚举a,在此基础上,在大于等于a的部分当中寻找b和c的组合。由于$a\le b \le c$,那么a一定小于等于0。所以我们只需要在小于等于0的范围内枚举a,在大于等于a的范围内枚举b和c即可,这样就去掉了大部分无谓的组合,减小了搜索空间,提升了算法的效率。 52 | 53 | 54 | 55 | 到这里我们又很容易发现,无论是要在小于等于0的范围内枚举a,还是要在大于等于a的范围内枚举b和c,我们都需要数组元素有序。所以我们可以先对数组排序,使得数组中元素有序,接着在小于等于0的范围内枚举a,在a的右侧枚举b和c,寻找b+c=-a的组合。寻找b和c的过程,本质上是一个寻找两数和的问题。对于两数和的问题,我们已经比较熟悉了,我们当然可以使用map来维护,但由于元素已经有序了,我们也可以使用双指针直接来寻找。 56 | 57 | 58 | 59 | 完整代码如下: 60 | 61 | 62 | 63 | ```C++ 64 | class Solution { 65 | public: 66 | vector> threeSum(vector& nums) { 67 | int n = nums.size(); 68 | // 数组排序 69 | sort(nums.begin(), nums.end()); 70 | vector> ret; 71 | // a+b+c=0, a <= b <= c, 枚举a 72 | for(int i = 0; i < n-2; i++) { 73 | // a大于0跳出 74 | if (nums[i] > 0) break; 75 | // 剪枝,去除重复搜索 76 | if (i > 0 && nums[i] == nums[i-1]) continue; 77 | int l = i+1; 78 | int r = n-1; 79 | 80 | while (l < r) { 81 | // target = a + b + c, 根据target和0的关系决定是右移l或左移r 82 | int target = nums[i] + nums[l] + nums[r]; 83 | if (target == 0) { 84 | vector cur {nums[i], nums[l], nums[r]}; 85 | ret.push_back(cur); 86 | l++; r--; 87 | // 跳过重复 88 | while (l < r && nums[l] == nums[l-1]) l++; 89 | // 跳过重复 90 | while (l < r && nums[r] == nums[r+1]) r--; 91 | }else if (target > 0) r--; 92 | else l++; 93 | } 94 | } 95 | return ret; 96 | } 97 | }; 98 | ``` 99 | 100 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](./resource/EasyLeetCode.png) 2 | 3 | 对于初学者来说,刷题是一件非常痛苦的事情。
4 | 其中的痛苦除了题目本身的难度带来之外,更多地是由于知识体系零碎导致的。想要获得 AC 需要语言基础、算法、数据结构三方面的配合,本 repo 旨在于将刷题用到的所有知识内容整合起来,都可以在一个 repo 中找到。减少查阅资料所需要的时间。
5 | C++ 相关文章每天一更,LeetCode 题解一周一更或两更
6 | 文章首发于公众号:`Coder梁`,可以关注获取最新文章
7 | 本人才疏学浅,文章可以看成是本人的学习笔记,有所谬误在所难免。欢迎各位大佬指教 8 | 9 | [![](https://img.shields.io/badge/%E5%85%AC%E4%BC%97%E5%8F%B7-Coder梁-green)](#公众号) 10 | [![](https://img.shields.io/badge/zhihu-%E7%9F%A5%E4%B9%8E-blue)](https://www.zhihu.com/people/coderliangt) 11 | [![](https://img.shields.io/badge/bilibili-B站-orange)](https://space.bilibili.com/266569818) 12 | 13 | ## 目录 14 | 15 | ### LeetCode 16 | 17 | - [1-两数之和](./LeetCode/01-two%20sum.md) 18 | - [2-两数相加](./LeetCode/02-add%20two%20number.md) 19 | - [4-寻找两个正序数组的中位数](./LeetCode/04-median%20of%20two%20sorted%20arrays.md) 20 | - [5-最长回文子串](./LeetCode/05-longest%20palindromic%20substring.md) 21 | - [10-正则表达式匹配](./LeetCode/10-regular%20expression%20matching.md) 22 | - [11-盛最多水的容器](./LeetCode/11-container%20with%20most%20water.md) 23 | - [15-三数之和](./LeetCode/15-3sum.md) 24 | 25 | ### C++ 26 | 27 | - [1-C++概述](./C++/1-概述.md) 28 | - [2-常用语句](./C++/2-常用语句.md) 29 | - [3-编码规范](./C++/3-谷歌编码规范.md) 30 | - [4-整型](./C++/4-整型.md) 31 | - [5-long long 与\_\_int64](./C++/5-long%20long与__int64.md) 32 | - [6-char 类型与 io 加速](./C++/6-char类型与io加速.md) 33 | - [7-浮点型](./C++/7-浮点数.md) 34 | - [8-算术运算符与类型转换](./C++/8-算术运算符与类型转换.md) 35 | - [9-数组的定义与初始化](./C++/9-数组的定义和初始化.md) 36 | - [10-字符串初体验](./C++/10-字符串创建与使用.md) 37 | - [11-C 风格字符串函数大全](./C++/11-cstring函数大全.md) 38 | - [12-string 类的用法](./C++/12-string用法.md) 39 | - [13-结构体初探](./C++/13-结构体初探.md) 40 | - [14-枚举类型](./C++/14-枚举类型.md) 41 | - [15-指针初探](./C++/15-指针初探.md) 42 | - [16-指针初探(二)](./C++/16-指针初探(二).md) 43 | - [17-指针初探(三)](./C++/17-指针初探(三).md) 44 | - [18-C++内存模型简介](./C++/18-内存模型初探.md) 45 | - [19-for 循环](./C++/19-for循环.md) 46 | - [20-自增与自减](./C++/20-自增与自减.md) 47 | - [21-while 循环与 do while 循环](./C++/21-while与do%20while循环.md) 48 | - [22-二维与多维数组](./C++/22-二维与多维数组.md) 49 | - [23-if 语句](./C++/23-if语句.md) 50 | - [24-逻辑表达式](./C++/24-逻辑表达式.md) 51 | - [25-三元表达式](./C++/25-三元表达式.md) 52 | - [26-switch 语句](./C++/26-switch语句.md) 53 | - [27-break 和 continue 语句](./C++/27-break和continue语句.md) 54 | - [28-指针和 const](./C++/28-指针和const.md) 55 | - [29-函数指针](./C++/29-函数指针.md) 56 | - [30-函数指针进阶](./C++/30-函数指针进阶.md) 57 | - [31-内联函数](./C++/31-内联函数.md) 58 | - [32-引用的使用](./C++/32-引用.md) 59 | - [33-引用与 const](./C++/33-引用与const.md) 60 | - [34-引用与指针的区别](./C++/34-引用和指针的区别.md) 61 | - [35-引用与结构体](./C++/35-引用结构体.md) 62 | - [36-默认参数](./C++/36-默认参数.md) 63 | - [37-函数重载](./C++/37-函数重载.md) 64 | - [38-右值引用](./C++/38-右值引用.md) 65 | - [39-函数模板](./C++/39-函数模板.md) 66 | - [40-重载模板](./C++/40-重载模板.md) 67 | - [41-模板显式具体化](./C++/41-模板显式具体化.md) 68 | - [42-模板实例化](./C++/42-模板实例化.md) 69 | - [43-编写头文件](./C++/43-编写头文件.md) 70 | - [44-联合编译](./C++/44-联合编译.md) 71 | - [45-自动存储连续性](./C++/45-自动存储连续性.md) 72 | - [46-auto 关键字](./C++/46-auto关键字.md) 73 | - [47-全局变量](./C++/47-全局变量.md) 74 | - [48-内部链接性](./C++/48-内部链接性.md) 75 | - [49-函数和语言链接性](./C++/49-函数和语言链接性.md) 76 | - [50-存储方案和动态分配](./C++/50-存储方案和动态分配.md) 77 | - [51-名称空间](./C++/51-名称空间.md) 78 | - [52-using 声明](./C++/52-using声明.md) 79 | - [53-using 声明和 using 编译指令](./C++/53-using声明和using指令.md) 80 | - [54-名称空间其他特性](./C++/54-名称空间其他特性.md) 81 | - [55-初探面向对象](./C++/55-初探面向对象.md) 82 | - [56-类的定义](./C++/56-类的定义.md) 83 | - [57-类的实现](./C++/57-类的实现.md) 84 | - [58-构造函数](./C++/58-构造函数.md) 85 | - [59-默认构造函数](./C++/59-默认构造函数.md) 86 | - [60-析构函数](./C++/60-析构函数.md) 87 | - [61-this 指针](./C++/61-this指针.md) 88 | - [62-类枚举](./C++/62-类枚举.md) 89 | - [63-抽象数据类型](./C++/63-抽象数据类型.md) 90 | - [64-运算符重载](./C++/64-运算符重载.md) 91 | - [65-重载限制](./C++/65-重载限制.md) 92 | - [66-友元函数](./C++/66-友元函数.md) 93 | - [67-重载<<运算符](./C++/67-重载<<运算符.md) 94 | - [68-类的转换](./C++/68-类的转换.md) 95 | - [69-转换函数](./C++/69-转换函数.md) 96 | - [70-构造函数的一些坑](./C++/70-构造函数的一些坑.md) 97 | - [71-拷贝构造函数](./C++/71-%E6%8B%B7%E8%B4%9D%E6%9E%84%E9%80%A0%E5%87%BD%E6%95%B0.md) 98 | - [72-赋值运算符](./C++/72-%E8%B5%8B%E5%80%BC%E8%BF%90%E7%AE%97%E7%AC%A6.md) 99 | - [73-成员初始化列表](./C++/73-%E6%88%90%E5%91%98%E5%88%9D%E5%A7%8B%E5%8C%96%E5%88%97%E8%A1%A8.md) 100 | - [74-继承(一)](./C++/74-%E7%BB%A7%E6%89%BF%EF%BC%88%E4%B8%80%EF%BC%89.md) 101 | - [75-继承(二)](./C++/75-%E7%BB%A7%E6%89%BF%EF%BC%88%E4%BA%8C%EF%BC%89.md) 102 | - [76-继承(三)](./C++/76-%E7%BB%A7%E6%89%BF%EF%BC%88%E4%B8%89%EF%BC%89.md) 103 | - [77-多态](./C++/77-%E5%A4%9A%E6%80%81.md) 104 | - [78-静态联编与动态联编](./C++/78-%E9%9D%99%E6%80%81%E8%81%94%E7%BC%96%E5%92%8C%E5%8A%A8%E6%80%81%E8%81%94%E7%BC%96.md) 105 | - [79-虚函数](./C++/79-%E8%99%9A%E5%87%BD%E6%95%B0.md) 106 | - [80-protected](./C++/80-protected.md) 107 | 108 | #### 公众号 109 | 110 | ![](./resource/wechat_qrcode.jpg) 111 | -------------------------------------------------------------------------------- /resource/EasyLeetCode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moutsea/EasyLeetCode/2803e024a91d5c85f9142594595442569e7b09bd/resource/EasyLeetCode.png -------------------------------------------------------------------------------- /resource/wechat_qrcode.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moutsea/EasyLeetCode/2803e024a91d5c85f9142594595442569e7b09bd/resource/wechat_qrcode.jpg --------------------------------------------------------------------------------