Browse Source

Merge remote-tracking branch 'gitlab/master'

kindring 1 year ago
parent
commit
a3c87d3e0a
1 changed files with 395 additions and 0 deletions
  1. 395 0
      渐构/拆书/高质量程序设计指南(第三版.林悦编著).md

+ 395 - 0
渐构/拆书/高质量程序设计指南(第三版.林悦编著).md

@@ -615,3 +615,398 @@ Microsoft C++采用的方案就和Borland C++的不同
 有些人甚至错误地认为学会使用Visual C++ IDE就是学会了C++语言。这又是一个误区!  
 **首先掌握语言的特征及其使用方法,再学习具体的语言实现才是语言学习的正道!**
 
+## C++/C程序基本概念
+### 启动函数main()
+#### `#d` C++/C程序的入口
+C++/C程序的可执行部分都是由函数组成的,  
+main()就是所有程序中都应该提供的一个默认全局函数——   
+主函数——所有的C++/C程序都应该从函数main()开始执行。  
+但是语言实现本身并不提供main()的实现(它也不是一个库函数),  
+并且具体的语言实现环境可以决定是否用main()函数来作为用户应用程序的启动函数,  
+这是标准赋予语言实现的权利(又是一个“实现定义的行为”☺)。
+
+#### `#c` main 特殊的函数 
+虽然main不是C++/C的保留字  
+(因此你可以在其他地方使用main这个名字,比如作为类、名字空间或者成员函数等的名字),  
+但是你也不可以修改main()函数的名字。如果修改了main()的名字,  
+比如改为mymain,连接器就会报告类似的连接时错误:“unresolved external symbol _main”。  
+这是因为C++/C语言实现有一个启动函数,
+main()可以看作是一个回调函数。
+main()由我们来实现,但是不需要我们提供它的原型,  
+因为我们并不能在自己的程序中调用它,这又和普通的回调函数有所不同。
+
+#### `#e` 找不到main的错误
+例如,MS C++/C应用程序的启动函数为mainCRTStartup()或者WinMainCRT-Startup(),  
+同时在该函数的末尾调用了main()或者WinMain(),  
+然后以它们的返回值为参数调用库函数exit(),  
+因此也就默认了main()应该作为它的连接对象,  
+如果找不到这样一个函数定义,自然会报错了。  
+
+#### `#c` main 框架里的main
+基于应用程序框架(Application Framework,如MFC)生成的源代码中往往找不到main(),  
+这并不是说这样的程序中就不需要main(),而是应用程序框架把main()的实现隐藏起来了,  
+并且它的实现具有固定的模式,所以不需要程序员来编写。  
+在应用程序的连接阶段,框架会将包含main()实现的library加进来一起连接。
+
+#### `#e` arduino程序
+在嵌入式框架arduino中,就没有main函数的影子.  
+开发者只需要编写 setup 与 loop 中的内容即可实现一个简单的程序
+
+#### `#d` 标准main函数原型
+1. `main` 应该返回 `int` ,但是具体返回什么类型可以被[实现]扩展
+2. 所有实现都必须至少允许两种形式的`main`
+- 无参数形式
+```c++
+int main (){ /*....*/}
+```
+- 有参数形式
+> 并允许实现再参数`argv`后面增加任何需要的也是可选的参数  
+
+```c++
+int main (int argc, char *argv[])
+```
+
+#### `#e` 更多的main
+比如MS C++/C允许main()返回void,  
+以及增加第三个参数char* env[]等。  
+读者可参考编译器的帮助文档,以了解当前的编译器支持怎样的扩展形式。
+
+#### `#d` main返回int的特殊含义
+当main()返回int类型时,不同的返回值具有不同的含义。  
+当返回0时,表示程序正常结束;  
+返回任何非0值表示错误或者非正常退出。  
+exit()用main()的返回值作为返回操作系统的代码,  
+以指示程序执行的结果(当然你也可以在main()或其他函数内直接调用exit()来结束程序)。
+
+#### `#d` c++标准对于main的限制
+(1)不能重载。  
+(2)不能内联。  
+(3)不能定义为静态的。  
+(4)不能取其地址。  
+(5)不能由用户自己调用。
+
+#### 命令行参数
+
+##### `#c` main 适用性更强的程序
+我们可能希望可执行程序具有处理命令行参数的能力,  
+如常用的“dir X:\document /p /w”等DOS或UNIX命令
+
+##### `#d` 命令行参数
+命令行参数是由启动程序截获并打包成字符串数组后传递给main()的一个形参`argv`,  
+包括命令字(即可执行文件名称)在内的所有参数的个数则被传递给形参argc。
+
+#### `#e` 文件拷贝示例
+下面是一个`dos`时期的文件拷贝示例代码
+用法示例
+```dos
+mycopy C:\file.txt C:\newFile.txt
+```
+示例代码
+```c++
+// mycopy.c : copy file to a specified destination file.
+#include <stdio.h>
+int main(int argCount, char* argValue[])
+{
+      FILE *srcFile = 0, *destFile = 0;
+      int ch = 0;
+      if (argCount != 3) {
+        printf("Usage: %s src-file-name dest-file-name\n", argValue[0]);
+      } else {
+        if (( srcFile = fopen(argValue[1], "r")) == 0) {
+        printf("Can not open source file \"%s\" !", argValue[1]);
+      } else {
+        if ((destFile = fopen( argValue[2], "w")) == 0) {
+              printf("Can not open destination file \"%s\"!", argValue[2]);
+              fclose(srcFile);  /*!!!*/
+        } else {
+              while((ch = fgetc(srcFile)) != EOF) fputc(ch, destFile);
+              printf("Successful to copy a file!\n");
+              fclose(srcFile);   /*!!!*/
+              fclose(destFile);  /*!!!*/
+              return 0;       /*!!!*/
+        }
+      }
+  }
+  return 1;
+}
+// 用法示例:
+mycopy C:\file1.dat C:\newfile.dat
+```
+
+#### 内部名称
+##### `#d` 什么是内部名称
+C和C++语言实现都会按照特定的规则把用户(指程序员)  
+定义的标识符(各种函数、变量、类型及名字空间等)转换为相应的内部名称。  
+当然,这些内部名称的命名方法还与用户为它们指定的连接规范有关,  
+比如使用C的连接规范,则main的内部名称就是_main。
+
+##### `#d` 内部名称(重命名Name-Mangling)的作用
+用于更好地区分函数
+不同的`实现`会采取不同的`Name-Mangling`方案
+
+##### `#c` 内部名称 编译器与函数
+在C语言中,所有函数不是局部于编译单元(文件作用域)的static函数,  
+就是具有extern连接类型和global作用域的全局函数,  
+因此除了两个分别位于不同编译单元中的static函数可以同名外,  
+全局函数是不能同名的;全局变量也是同样的道理。  
+其原因是C语言采用了一种极其简单的函数名称区分规则:  
+仅在所有函数名的前面添加前缀“_”,  
+从唯一识别函数的作用上来说,  
+实际上和不添加前缀没什么不同。
+但是,C++语言允许用户在不同的作用域中定义同名的函数、类型、变量等,  
+这些作用域不仅限于编译单元,还包括class、struct、union、namespace等,  
+甚至在同一个作用域中也可定义同名的函数,即重载函数。  
+那么编译器和连接器如何区分这些同名且又都会在同一个编译单元中被引用的程序元素呢?  
+编译器如何识别下面的foo是调用哪个函数呢?
+```c++
+
+    Sample_1  a;
+    Sample_2  b;
+    a.foo(“aaa”);
+    a.foo(100);
+    b.foo(“bbb”);
+    b.foo(false);
+```
+在连接器看来,所有函数都是全局函数,  
+能够用来区分不同函数调用的除了作用域外就是函数名称了。  
+但是,上面的调用显然都是合理合法的。  
+因此,如果不对它们进行重命名,就会导致连接二义性。
+在C++中,重命名称为`“Name-Mangling”(名字修饰或名字改编)`
+
+##### `#e` 重命名规则示例
+例如,在它们的前面分别添加所属各级作用域的名称(class、namespace等)  
+及重载函数的经过编码的参数信息(参数类型和个数等)作为前缀或者后缀,  
+产生全局名字  
+Sample_1_foo@pch@1、  
+Sample_1_foo@int@1、  
+Sample_2_foo@pch@1  
+和Sample_2_foo@int@1,  
+这样就可以区分了。  
+关于这方面更详细的信息请参考Lippman的  
+《Inside The C++ Object Model》相关章节,  
+你也可以从MS C++/C编译器输出的  
+MAP文件了解一下它所Mangling出来的函数的内部名称。
+
+
+#### 连接规范
+##### `#d` 为什么要有连接规范
+1. 不同`语言`联合开发共享接口  
+2. 通用连接规范`extern "C"`  
+3. 声明与实现应当同一规范  
+
+##### `#e` 针对类型,函数,变量等指定连接规范
+```c
+extern "C" void WinMainCRTStartup();
+extern "C" const CLSID CLSID_DataConverter;
+extern "C" struct Student{……};
+extern "C" Student g_Student;
+```
+
+##### `#e` 针对一段代码限定连接规范
+```
+#ifdef  __cplusplus
+extern "C" {
+#endif
+const int MAX_AGE = 200;
+#pragma pack(push, 4)
+typedef struct _Person
+{
+    char *m_Name;
+    int   m_Age;
+} Person, *PersonPtr;
+#pragma pack(pop)
+Person g_Me;
+int    __cdecl  memcmp(const void*,const void*,size_t);
+void*  __cdecl  memcpy(void*,const void*,size_t);
+void*  __cdecl  memset(void*,int,size_t);
+#ifdef  __cplusplus
+}
+#endif
+```
+
+##### `#e` 混合使用连接规范
+如果当前使用的是C++编译器,  
+并且使用了extern“C”来限定一段代码的连接规范,  
+但是又想令其中某行或某段代码保持C++的连接规范,  
+则可以编写如下代码(具体要看你的编译器是否支持extern“C++”)
+```
+#ifdef  __cplusplus
+extern "C" {
+#endif
+const int MAX_AGE = 200;
+#pragma pack(push, 4)
+typedef struct _Person
+{
+    char *m_Name;
+    int   m_Age;
+} Person, *PersonPtr;
+#pragma pack(pop)
+Person g_Me;
+#if  _SUPPORT_EXTERN_CPP_
+extern “C++” {
+#endif
+int    __cdecl  memcmp(const void*,const void*,size_t);
+void*  __cdecl  memcpy(void*,const void*,size_t);
+#if  _SUPPORT_EXTERN_CPP_
+}
+#endif
+void*  __cdecl  memset(void*,int,size_t);
+#ifdef  __cplusplus
+}
+#endif
+```
+
+##### `#e` 声明与实现
+> 这段我实在看不明白写的是什么意思,每个字我都认识  
+
+如果在某个声明中指定了某个标识符的连接规范为extern“C”,  
+那么也要为其对应的定义指定extern“C”连接规范,如下所示:
+```
+#ifdef  __cplusplus
+extern "C" {
+#endif
+int  __cdecl  memcmp(const void*,const void*,size_t);  // 声明
+#ifdef  __cplusplus
+}
+#endif
+#ifdef  __cplusplus
+extern "C" {
+#endif
+int  __cdecl  memcmp(const void*p,const void*a,size_t len)
+{
+  ……  // 功能实现
+}
+#ifdef  __cplusplus
+}
+#endif
+```
+
+##### `#c` 碎碎念 特殊的COM接口方法
+但是对COM接口方法  
+(Interface Methods,Interface中的pure virtual functions)  
+使用的C复合数据类型来说(它们也是COM对象接口的组成部分),  
+是否采用统一的连接规范,  
+对COM对象及组件的二进制数据兼容性和可移植性都没有影响。  
+因为即使接口两端(COM接口实现端和接口调用端)  
+对接口数据类型的内部命名不同,  
+只要它们使用了一致的成员对齐和排列方式、一致的调用规范、  
+一致的virtual function实现方式,  
+总之就是一致的C++对象模型,  
+并且保证COM组件升级时不改变原来的接口和数据类型定义,  
+则所有方法的运行时绑定和参数传递都不会存在问题  
+(所有方法的调用都被转换为通过对象指针对vptr和vtable以及函数指针的访问和调用,  
+这种间接性不再需要任何方法名即函数名的参与,  
+而接口名和方法名只是为了让客户端的代码能够顺利通过编译,  
+但是连接时就全部不再需要了)。
+
+#### 变量与其初始化
+##### `#d` 什么是变量
+变量就是用来保存数据的程序元素,它是内存单元的别名,  
+取一个变量的值就是读取其内存单元中存放的值,  
+而写一个变量就是把值写入到它代表的内存单元中
+
+##### `#d` 变量声明
+全局变量的声明和定义应当放在源文件的开头位置。  
+在C++/C中,全局变量(extern或static的)存放在程序的静态数据区中  
+如果你没有明确地给全局变量提供初始值,编译器会自动地将0转换为所需要的类型来初始化它们  
+一个编译单元中定义的全局变量的初始值不要依赖定义于另一个编译单元中的全局变量的初始值  
+
+##### `#d` 初始值依赖与编译顺序
+编译器和连接器可以决定同一个编译单元中定义的全局变量的  
+初始化顺序保持与它们定义的先后顺序一致,  
+但是却无法决定当两个编译单元连接在一起时  
+哪一个的全局变量的初始化先于另一个编译单元的全局变量的初始化。  
+也就是说,这一次编译连接和下一次编译连接很可能使  
+不同编译单元之间的全局变量的初始化顺序发生改变。
+
+#### 运行时库(C Runtime Library)
+##### `#c` 运行时 运行时库
+一般来说,一个C++/C程序不可能不使用C运行时库,  
+即使你没有显式地调用其中的函数也可能间接地调用,  
+只是我们平时没有在意罢了。  
+例如,启动函数、I/O系统函数、存储管理、RTTI、动态决议、  
+动态链接库(DLL)等都会调用C运行时库中的函数。  
+我们在每一个程序开头包含的stdio.h头文件中的许多I/O函数就是它的一部分。  
+C运行时库有多线程版和单线程版,  
+开发多线程应用程序时应该使用多线程版本的库,  
+仅在开发单线程程序时才使用单线程版本。  
+另外,同一软件的不同模块最好使用一致的运行时库,否则会出现连接问题。
+
+#### 运行时与编译时
+##### `#d` 什么是运行时与编译时
+我们把编译预处理器、编译器和连接器工作的阶段合称“编译时”。  
+语言中有些构造仅在编译时起作用,而有些构造则是在“运行时”起作用的,  
+分清楚这些构造对于程序设计很重要。  
+
+##### `#e` 编译时中的内容
+例如,预编译伪指令、类(型)定义、外部对象声明、函数原型、  
+标识符、各种修饰符号(const、static等)  
+及类成员的访问说明符(public、private、protected)  
+和连接规范、调用规范等,  
+仅在编译器进行语法检查、语义检查和生成目标文件(.obj或.o文件)  
+及连接的时候起作用的,在可执行程序中不存在这些东西。  
+
+##### `#e` 运行时内容
+容器越界访问、虚函数动态决议、函数动态连接、动态内存分配、  
+异常处理和RTTI等则是在运行时才会出现和发挥作用的,  
+因此运行时出现的程序问题大多与这些构造有关。
+
+##### `#e` 运行时异常
+下面代码在编译时绝对没有问题,但是运行时会出现错误  
+```c++
+int *pInt = new int[10];
+pInt+=100;          // 越界,但是还没有形成越界访问
+cout<<*pInt<<endl;   // 越界访问!可能行,也可能不行!
+*pInt=1000;         // 越界访问!即使偶尔不出问题,但不能确保永远不出问题!
+```
+
+##### `#e` 不太标准的程序
+下述代码在编译时没有问题,在运行时也不会出现错误,但是违背了private的用意。  
+```
+class Base {
+public:
+  virtual void Say(){ cout<< "Base::Say() was invoked!\n"; }
+};
+class Derived : public Base {
+private:      // 改变访问权限,合法但不是好风格!
+  virtual  void Say(){cout<<“Derived::Say()was invoked!\n”;}
+};
+// 测试
+Base *p = new Derived;
+p->Say();    // 输出:Derived::Say()was invoked!
+             // 出乎意料地绑定到了一个private函数身上!
+```
+
+##### `#c` 经验 推测运行时行为
+我们在程序设计时就要对运行时的行为有所预见,  
+通过编译连接的程序在运行时不见得就是正确的。  
+虽然你能够一时“欺骗”编译器(因为编译器还不够聪明),  
+但是由此造成的后果要你自己来承担。  
+这里我们引用Bjarne Stroustrup的一段话来说明这一问题:  
+“C++的访问控制策略是为了防止意外事件而不是防止对编译器的故意欺骗。  
+任何程序设计语言,只要它支持对原始存储器的直接访问(如C++的指针),  
+就会使数据处于一种开放的状态,  
+使所有有意按照某种违反数据项原有类型安全规则所描述的方式去触动它的企图都能够实现,  
+除非该数据项受到操作系统的直接保护。”
+
+
+#### 编译单元
+##### `#d` 什么是编译单元
+语言实现和开发环境支持的独立编译技术并非语言本身所规定的。  
+每一个源代码文件(源文件及其递归包含的所有头文件展开)  
+就是一个最小的编译单元,  
+每一个编译单元可以独立编译而不需要知道其他编译单元的存在及其编译结果。 
+
+##### `#e` 编译单元的编译
+例如,一个编译单元在单独编译的时候根本无法知道  
+另一个编译单元在编译的时候是否已经定义了一个同名的extern全局变量或全局函数,  
+所以每个编译单元都能够通过编译,  
+但是如果另一个编译单元也定义了同名的extern全局变量或全局函数,  
+那么将两个目标文件连接到一起的时候就会出错。  
+
+##### `#d` 独立编译
+独立编译技术最大的好处就是公开接口而隐藏实现,  
+并可以创建预定义的二进制可重用程序库(函数库、类库、组件库等),  
+在需要的时候用连接器将用户代码与库代码连接成可执行程序。  
+另一方面,独立编译技术可以大大减少代码修改后重新编译的时间。
+