CPP----高质量编程

1.防止头文件重复包含应使用 #define 来防止头文件被多重包含, 命名格式当是: <PROJECT>_<PATH>_<FILE>_H_ .例如, 项目 foo 中的头文件 foo/src/bar/baz.h 可按如下方式保护:

#ifndef FOO_BAR_BAZ_H_
#define FOO_BAR_BAZ_H_
...
#endif // FOO_BAR_BAZ_H_

2.前置声明的主要作用:1)避免重复定义变量;2)避免引入函数定义/声明文件,从而函数文件发生更改时不会重新编译依赖文件;3)解决循环依赖问题。前置声明只能作为指针或引用,不能定义类的对象,自然也就不能调用对象中的方法了。需要注意,如果将类A的成员变量B b;改写成B& b;的话,必须要将b在A类的构造函数中,采用初始化列表的方式初始化,否则也会出错。
尽可能地避免使用前置声明。使用 #include 包含需要的头文件即可;
*

3.只有当函数只有 10 行甚至更少时才将其定义为内联函数.析构函数往往比其表面看起来要更长, 因为有隐含的成员和基类析构函数被调用,因此析构函数定义为内联要特别小心;

4.包含文件的名称使用 . 和 … 虽然方便却容易导致混乱, 使用比较完整的项目路径看上去更清晰。例如,google-awesome-project/src/base/logging.h 应该按如下方式包含:#include "base/logging.h"。源文件对应的头文件应该优先包含,这样可以保证当这个头文件遗漏某些必要的库时,源文件的构建会立刻终止。因此这一条规则保证维护这些文件的人们首先看到构建中止的消息而不是维护其他包的人们。例如:针对dir/http://foo.cc的头文件包含顺序:1)dir2/foo2.h (优先位置) 2)C 系统文件3)C++ 系统文件 4)其他库的 .h 文件 5)本项目内 .h 文件。例如,google-awesome-project/src/foo/internal/http://fooserver.cc 的包含次序如下:

#include "foo/public/fooserver.h" // 优先位置 
#include <sys/types.h> 
#include <unistd.h> 
#include <hash_map> 
#include <vector> 
#include "base/basictypes.h" 
#include "base/commandlineflags.h" 
#include "foo/public/bar.h"

5.将函数变量尽可能置于最小作用域内, 并在变量声明时进行初始化.

6.匿名命名空间可避免命名冲突, 限定作用域, 避免直接使用 using 关键字污染命名空间;

7.构造函数不允许调用虚函数,这类调用是不会重定向到子类的虚函数实现. 即使当前没有子类化实现, 将来仍是隐患.(这个时候虚函数指针还没有正常初始化?)

8.类型转换运算符和单参数构造函数都应当用 explicit 进行标记. 一个例外是, 拷贝和移动构造函数不应当被标记为 explicit, 因为它们并不执行类型转换. 这样比较健壮。

9.仅当只有数据成员时使用 struct, 其它一概使用 class.

10.真正需要用到多重实现继承的情况少之又少. 只在以下情况我们才允许多重继承: 最多只有一个基类是非抽象类; 其它基类都是以 Interface 为后缀的 纯接口类.

11.除少数特定环境外, 不要重载运算符. 也不要创建用户定义字面量.

12.类定义一般应以 public: 开始, 后跟 protected:, 最后是 private:. 省略空部分. 所有数据成员声明为 private;

13.函数的参数顺序为: 输入参数在先, 后跟输出参数.

14.所有按引用传递的参数必须加上 const,输出参数用指针。

15.使用 cpplint.py 检查风格错误.在行尾加 // NOLINT, 或在上一行加 // NOLINTNEXTLINE, 可以忽略报错. (
http://github.com/google/styleguide/blob/gh-pages/cpplint/cpplint.py)

16.对于迭代器和其他模板对象使用前缀形式 (++i) 的自增, 自减运算符.对简单数值 (非对象), 两种都无所谓.

17.整数用 0, 实数用 0.0, 指针用 nullptr 或 NULL, 字符 (串) 用 ‘\0’.

18.尽可能用 sizeof(varname) 代替 sizeof(type).

19.auto 只能用在局部变量里用。别用在文件作用域变量,命名空间作用域变量和类数据成员里。永远别列表初始化 auto 变量。

20.lamda函数禁用默认捕获,捕获都要显式写出来。打比方,比起 [=](int x) {return x + n;}, 您该写成 [n](int x) {return x + n;} 才对,这样读者也好一眼看出 n 是被捕获的值。匿名函数始终要简短,如果函数体超过了五行,那么还不如起名或改用函数。如果可读性更好,就显式写出 lambd 的尾置返回类型,就像auto.

21.不要使用复杂的模板编程

22.函数命名, 变量命名, 文件命名要有描述性; 少用缩写.注意, 一些特定的广为人知的缩写是允许的, 例如用 i 表示迭代变量和用 T 表示模板参数.

23.文件名要全部小写, 可以包含下划线 () 或连字符 (-), 依照项目的约定. 如果没有约定, 那么 “” 更好.

24.类型名称的每个单词首字母均大写, 不包含下划线: MyExcitingClass, MyExcitingEnum

25.变量 (包括函数参数) 和数据成员名一律小写, 单词之间用下划线连接. 类的成员变量以下划线结尾, 但结构体的就不用, 如: a_local_variable, a_struct_data_member, a_class_data_member_.

26.声明为 constexpr 或 const 的变量, 或在程序运行期间其值始终保持不变的, 命名时以 “k” 开头, 大小写混合. 例如:const int kDaysInAWeek = 7;

27.常规函数使用大小写混合, 取值和设值函数则要求与变量名匹配:

MyExcitingFunction()MyExcitingMethod()my_exciting_member_variable()set_my_exciting_member_variable()

28.命名空间以小写字母命名. 最高级命名空间的名字取决于项目名称. 要注意避免嵌套命名空间的名字之间和常见的顶级命名空间的名字之间发生冲突.

29.枚举的命名应当和常量或宏一致: kEnumName或是 ENUM_NAME

30.在每一个文件开头加入版权公告.

31.每个类的定义都要附带一份注释, 描述类的功能和用法, 除非它的功能相当明显.

32.函数声明处的注释描述函数功能; 定义处的注释描述函数实现.

33.对那些临时的, 短期的解决方案, 或已经够好但仍不完美的代码使用 TODO 注释.

// TODO(kl@gmail.com): Use a "*" here for concatenation operator.

// TODO(Zeke) change this to use relations.

// TODO(bug 12345): remove the "Last visitors" feature

34.每一行代码字符数不超过 80. 包含长路径的 #include 语句可以超出80列. 头文件保护 可以无视该原则.

35.只使用空格, 每次缩进 2 个空格.

36.返回类型和函数名在同一行, 参数也尽量放在同一行, 如果放不下就对形参分行, 分行方式与 函数调用 一致.缺省缩进为 2 个空格.换行后的参数保持 4 个空格的缩进.

ReturnType LongClassName::ReallyReallyReallyLongFunctionName(
    Type par_name1,  // 4 space indent
    Type par_name2,
    Type par_name3) {
  DoSomething();  // 2 space indent
  ...
}

37.倾向于不在圆括号内使用空格. 关键字 if 和 else 另起一行.

38.所有情况下 if 和左圆括号间都有个空格. 右圆括号和左大括号之间也要有个空格

if (condition) {  // 圆括号里没有空格.
  ...  // 2 空格缩进.
} else if (...) {  // else 与 if 的右括号同一行.
  ...
} else {
  ...
}

39.switch 语句中的 case 块可以使用大括号也可以不用, 取决于你的个人喜好. 如果用的话, 要按照下文所述的方法.如果有不满足 case 条件的枚举值, switch 应该总是包含一个 default 匹配 (如果有输入值没有 case 去处理, 编译器将给出 warning). 如果 default 应该永远执行不到, 简单的加条 assert:

40.如果一个布尔表达式超过标准行宽, 断行方式要统一.
下例中, 逻辑与 (&&) 操作符总位于行尾:

if (this_one_thing > this_other_thing &&
    a_third_thing == a_fourth_thing &&
    yet_another && last_one) {
  ...
}

41.命名空间内容不要缩进.

42.水平留白的使用根据在代码中的位置决定. 永远不要在行尾添加没意义的留白.垂直留白越少越好.

43. 头文件中只存放声明,而不存放定义,注意:C++语法中,类的成员函数可以在声明的同时被定义,并且自动成为内联函数,这虽然会带来书写上的方便,但却造成了风格不一致,建议将成员函数的定义与声明分开,不论该函数体有多么小。

44. 不提倡使用全局变量,尽量不要再头文件中出现 exter int value这类声明。

45.一行代码只做一件事情,如只定义一个变量,或只写一条语句。

46. 尽可能在定义变量的同时初始化该变量。

47. if语句(1)不可将布尔变量直接与true,false或者1,0进行比较。根据布尔类型的语义,零值为"假" (false),任何非零值都是“真”(true),true的值究竟是什么并没有统一的标准;(2)应当将整型变量用 == 或 != 直接与 0 比较; (3)不可将浮点变量用 == 或 != 与任何数字比较,无论是float还是double类型的变量,都有精度限制,所以一定要避免将浮点类型变量用 == 与数字比较,应该设法转换成 >= 或 <= 形式; (4)指针变量直接与 NULL比较,而不是与 0比较,尽管NULL的值与0相同,但是两者的意义不同, if (NULL == p) 和 if (p == NULL)的区别?因为NULL不能被赋值,如果漏写成 NULL = p,编译器会直接报错,但 如果漏写成 p = NULL,不会报错

//1 bool flag
if (flag)//真
if (!flag)//假
//不良风格
if (flag == true)
if (flag == 0)

//2 int value
if (value == 0)
//不良风格 误解是bool
if (!value)

//3 float x
if (x >= erp)
//不良风格
if (x == 0.0)

48. for循环语句 (1)在多重循环中,如果有可能,应当将最长的循环放在最内层,最短的循环放在最外层,以减少 CPU跨切循环层的次数,效率会提高;

//低效率
for (int i =0 ; i <100; i++)
{
 for (int j =0 ; j< 5; j++)
 {
  sum += a[i][j];
 }
}
//高效率 长循环在内层
for (int i =0 ; i <5; i++)
{
 for (int j =0 ; j< 100; j++)
 {
  sum += a[i][j];
 }
}

(2)如果循环体内存在逻辑判断,并且循次数很大,将逻辑判断移到循环体的外面,如果在内部,就会多执行很多次逻辑判断,并且破坏了循环“流水线”作业,使得编译器不能对循环进行优化处理,降低了效率。

//效率低
for (int i =0; i < N; i++)
{
 if(cond)
 {
   DO();
 }
 else
 {
  DONothing();
 }
}
//高效率但是不简洁
if (cond)
{
 for (int i =0 ;i < N ; i++)
 {
  DO();
 }
}
else
{
 for (int i =0 ;i < N ; i++)
 {
  DONothing();
 }
}

(3)不可在for循环体内修改循环变量,防止 for循环失去控制。
(4)建议for语句的循环控制变量的取值采用 半开闭区间的写法

//半开半闭
for (int i = 0; i < N; i++)
//闭区间 不建议
for (int i = 0 ; i <= N -1; i++)

49. goto语句不建议用,当不是禁止用,goto语句至少有一处可显神通,它能从多重循环体中一下跳到外面,不用写很多次 break语句。

{
 {
  {
   goto error;
  }
 }
}

50. const常量有数据类型,而#define没有数据类型,编译器可以对前者进行类型安全检查,而只对后者进行字符替换,没有类型安全检查。const常量完全可以取代宏常量。
(1)不能在类声明中初始化 const数据成员,因为类的对象未被创建时,编译器不知道 SIZE的值是多少。

class A
{
 //错误,在类声明中初始化const数据成员
 const int SIZE = 100;
 //错误,未知的SIZE
 int array[SIZE];
};

(2)const 数据成员的初始化只能在类构造函数的初始化表中进行。

class A
{
 A(int size);
 const int SIZE;
};
A::A(int size):SIZE(size)
{

}
A a(200);
A b(100);

(3)通过类中的枚举常量实现在整个类中都恒定的常量,不用指望const数据成员了。枚举常量不会占用对象的存储空间,它们在编译时被全部求值,其缺点是:它的隐含数据类型是整数,其最大值有限,且不能表示浮点数。

class A
{
 enum{SIZE =100, SIZE2 =200};
 int array1[SIZE];
 int arrray2[SIZE2];
};
  1. 函数设计
    (1)如果函数没有参数,则用 void填充。
float GetValue(void);//良好风格
float GetValue();//不良风格

(2)目的参数放在前面,源参数放在后面
(3)如果参数是指针,且仅作输入用,则应在类型前加 const,以防止该指针在函数体内被意外修改。void StringCopy(char *Des, const char *str)
(4)如果输入的参数以值传递的方式传递对象,则改用 const & 方式来传递,这样可以省去临时对象的构造和析构过程,从而提高效率。
(5)如果函数的返回值是一个对象,有些场合用引用传递替换值传递可以提高效率,而有些场合只能用值传递而不能用引用传递,否则会出错。

class STring
{
 //赋值函数
 String & operate==(const STring &other);
    //相加函数,如果没有 friend修饰只允许有一个右侧参数
    friend String operate+(const STring &s1, const String &s2);
   private:
    char *m_data;
};
//赋值函数的实现
String & String::operate=(const String &other)
{
 if (this == &other)
 {
  return *this;
 }
 m_Data = new char[strlen(other.data) + 1];
 strcpy(m_data, other.data);
 //返回的是 *this的引用,无需拷贝过程
 retrun *this;
}

赋值函数,应用引用传递的方式返回String 对象,如果用值传递的方式,虽然功能仍然正确,但由于 return 语句要把 *this 拷贝到保存返回值的外部存储单元之中,增加了不必要的开销,降低了赋值函数的效率。

String a,b,c;
//如果用值传递,将产生一次 *this的拷贝
a =b;
//将产生两次的 *this拷贝
a=b=c;

String 的相加函数 operate++ 的实现如下。

String operate+(const String &s1, const String &s2)
{
 String temp;//值传递
 delete temp.data;
 temp.data = new char[strlen(s1.data) + strlen(s2.data) +1];
 strcpy(temp.data,s1.data);
 strcat(temp.data,s2.data);
 return temp;
}

对于相加函数,应用值传递的方式返回String对象,如果改用 引用传递,那么函数返回值是一个指向局部对象 temp的引用,由于temp在函数结束时被自动销毁,将导致返回的引用无效。c =a +b;//此时a+b并不返回期望值,c什么也得不到,留下隐患。
(6)return语句不可返回指向 栈内存 的指针或者引用,因为该内存在函数体结束时被自动销毁

char *FUN(void)
{
 //str的内存位于栈上
 char str[] = “hello"'
 //将导致错误
 return str;
}

如果函数返回值是一个对象,要考虑return语句的效率。创建一个临时对象并返回它,如下。return String(s1+s2);编译器直接把临时对象创建并初始化在外部存储单元中,省去了拷贝和析构的消耗,提高了效率。区别:先创建一个局部对象 temp并返回它的结果。String temp(s1+s2);return temp;上述代码将发生三件事:1,首先temp对象被创建,同时完成初始化;2,然后拷贝构造函数把temp拷贝到保存返回值的外部存储单元中;3,最后,temp在函数结束时被销毁,调用析构函数。由于内部数据类型如 int,float ,double的变量不存在构造函数和析构函数,虽然该临时变量的语法不会提高多少效率,但是程序更加简洁易读。
(7)assert不是函数,而是宏
(8)引用与指针的区别。1,int m; int &n = m,n是m的一个引用,m是被引用物,n相当于m的别名,对n的任何操作就是对m的操作。所以n即不是m的拷贝,也不是指向m的指针,其实n就是m它自己。2,引用被创建的同时必须被初始化,指针则可以在任何时候被初始化;3,不能有NULL引用,引用必须与合法的存储单元关联,指针则可以是NULL;4, 一旦引用被初始化,就不能改变引用的关系,指针则可以随时改变所指的对象;int i =5; int j =6; int &k = i; k = j;k和i的值都变成了6。

52. 内存的分配方式
1,静态存储区域分配:内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在,例如全局变量,static变量;2,在栈上创建:在执行函数时候,函数内部局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限;
3,堆内存:动态内存分配,程序在运行时候用malloc或new申请任意多少的内存,程序员自己负责在何时用 free或delete释放内存,动态内存的生存期由我们决定,使用灵活,但是问题也多。

53. 内存使用错误
1,内存分配未成功,却使用了它使用之前检查是否存在,如果是函数入口,可以 assert(p != NULL),如果是malloc或new申请内存,可以 if (NULL == p)进行防错处理
2,内存分配虽然成功,但是尚未初始化就引用它注意内存的缺省值初值是什么并没有统一标准,不一定全为0,因此,在创建数组时候,别忘记赋初始值,赋0也不能省略。3,内存分配成功并且已经初始化,但操作越过了内存的边界,数组操作越界;
4,忘记了释放内存,造成内存泄漏,含有这种错误的函数每被调用一次就丢失一块内存,刚开始时系统的内存充足,你看不到错误,终有一次程序突然死掉,系统出现提示:内存耗尽,malloc/free,new/delete必须成对出现;5,释放了内存却继续使用它,(1)return语句写错了,注意不要返回指向 栈内存 的指针或者引用,因为该内存在函数体结束时被自动销毁,(2)free或delete释放了内存之后,没有将指针设置为NULL,导致产生了 野指针

54. 指针与数组对比
数组要么在静态存储区被创建,如全局数组,要么在栈上被创建。数组名对应着一块内存,其地址与容量在生命期内保持不变,只有数组的内容可以改变。指针可以随时指向任意类型的内存块,它的特征是 可变,所以我们常用指针来操作动态内存,指针远比数组灵活,但也更危险。

55. 字符串比较指针与数组

// 1,修改内容
//字符数组a的容量是 6个字符,内容 hello\0
char a[] = "hello";
//a的内容可以改变
a[0] = 'X';
//指针p指向常量字符串 "world",位于静态存储区,内容是 world\0
char *p = "world";
//常量字符串的内容是不可以被修改的
p[0] = 'X';//运行时出错,编译器不能发现该错误

56. 内容复制与比较
不能对数组名进行直接复制与比较,否则会产生编译错误;指针 p =a 并不能把 a的内容复制指针 p,而是把 a的地址赋给了p。要想复制 a的内容,可以先用库函数 malloc为p申请一块容量 为 strlen(a) +1个字符的内存,再用strcpy进行字符串复制。

//数组
char a[] = "hello";
char b[10];
//不能 b = a,而是
strcpy(b,a);
//不能 b ==a ,而是
if(strcmp(b,a) == 0)

//指针
int len =strlen(a)
char *p = (char*)malloc(sizeof(char)*(len+1));
//不能 p = a,而是
strcpy(p,a);
//不能 p ==a ,而是
if(strcmp(p,a) == 0)

57. 计算内存容量
sizeof计算数组的容量(字节数),并忘记 + “\0”。

char a[] = "hello world";
char *p =a;
sizeof(a);//12 加'\0'
sizeof(p);//4 得到的是一个指针变量的字节数,相当于 sizeof(char*),而不是p所指的内存容量
//当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针
void Func(char a[100])
{
 sizeof(a);//4 而不是100
}

58. 指针参数如何传递内存
1,如果函数的参数是一个指针,不要指望用该指针去申请动态内存。

void GetMemory(char *p ,int num)
{
 p = (char*)malloc(sizeof(char) * num);
}
void Test(void)
{
 char *str = NULL;
 //str仍然为NULL,没有获的期望的内存
 GetMemory(str,100)
 //运行错误
 strcpy(str,"hello");
}

编译器总是要为函数的每个参数制作临时副本,指针参数p的副本是 _p,编译器 使 _p =p,如果函数体内的程序修改了 _p的内容,就导致参数p的内容作相应的修改,这就是指针可以作为输出参数的原因。本例中,__p申请了新的内存,只是把 —p所指的内存地址改变了,但是p丝毫没变,所以该函数并不能输出任何东西。相反,每调用一次,就好泄漏一块内存,因为没有free释放。

  1. 可以用 指向指针的指针 实现用指针参数去申请内存
void GetMemory(char **p ,int num)
{
 *p = (char*)malloc(sizeof(char) * num);
}
void Test(void)
{
 char *str = NULL;
 //&str
 GetMemory(&str,100)
 //运行错误
 strcpy(str,"hello");
 cout<<str<<endl;
 free(str);
}
  1. 继续简化为函数返回值来传递动态内存
char *GetMemory(int num)
{
 p = (char*)malloc(sizeof(char) * num);
 return p;
}
void Test(void)
{
 char *str = NULL;
 
 str = GetMemory(100);
 //运行错误
 strcpy(str,"hello");
 free(str);
}
  1. 不要用 return语句返回指向 栈内存的指针,因为该内存在函数结束时自动消亡
char *GetMemory(int num)
{
 char p[] = "hello world";
 return p;//编译器将提出警告
}
void Test(void)
{
 char *str = NULL;
 
 str = GetMemory()//str的内容是垃圾,不是NULL
 //运行错误
 strcpy(str,"hello");
 free(str);
}
  1. free和delete把指针怎么了,它们只是把指针所指的内存给释放掉,当并没有把指针本身干掉。
char *p =(char*)malloc(100);
strcpy(p,”hello);
//p所指向的内存被释放,但是p所指的地址仍然不变
//不是NULL,只是该地址对应的内存是垃圾,p成了野指针
free(p).....
if(p != NULL)//没有起到防错作用
{
 //野指针,出错
 strcpy(p,"hello");
}
  1. 杜绝野指针 野指针不是NULL指针,是指向 垃圾内存的指针,野指针很危险,if拦不住,成为野指针的原因有。1,指针变量没有被初始化
    指针变量在创建的同时应当被初始化,要么将指针设置为 NULL,要么让它指向合法的内存。2,指针 p被free或者delete之后,没有设为NULL
    3,指针操作超越了变量的作用范围
char *p = NULL:
cahr *str = (char*)malloc(100);
class A
{
 public:
 void Func(void){ cout << “Func of class A<< endl; }
};
void Test(void)
{
 A *p;
 {
  A a;
  p = &a; // 注意 a 的生命期
 }
    //对象a已经消失,p指向a,所以p成了野指针
 p->Func(); // p 是“野指针”
}

7. malloc/free和new/delete的区别
malloc/free是 C++/C语言标准的库函数,对于非内部数据类型的对象而言,它无法满足动态对象的要求,对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数,库函数不在编译器控制的权限之内,不能执行构造和析构。new/delete是C++运算符,能完成动态内存分配和初始化工作,如何实现动态内存管理。

class Obj
{
 public :
  Obj(void){ cout << “Initialization” << endl; }
  ~Obj(void){ cout << “Destroy” << endl; }
  void Initialize(void){ cout << “Initialization” << endl; }
  void Destroy(void){ cout << “Destroy” << endl; }
};
//malloc/free不能执行构造函数和析构函数,必须调用成员函数来完成初始化和清除工作
void UseMallocFree(void)
{
 Obj *a = (obj *)malloc(sizeof(obj)); // 申请动态内存
 a->Initialize(); // 初始化
 //…
 a->Destroy(); // 清除工作
 free(a); // 释放内存
}
//new/delete本身就可以完成构造和析构
void UseNewDelete(void)
{
 Obj *a = new Obj; // 申请动态内存并且初始化
 //…
 delete a; // 清除并且释放内存
}

如果用 free释放 new创建的动态对象,那么该对象因无法执行析构函数而可能导致程序出错;如果用delete释放malloc申请的动态内存,理论上程序不会出错,但是可读性差,因此,必须成对出现。

  1. 函数的高级特性
    重载 overloaded,内联 inline: 既可以用于全局函数也可用于类的成员函数;const和virtual仅仅用于类的成员函数。
  2. 重载
    语义,功能相似的几个函数用同一个名字表示,提高函数的易用性。并且,类的构造函数需要重载,C++规定构造函数于类同名,只有一个名字,如果想用几种不同的方法创建对象,只能用重载机制来实现,所以类可以有多个同名的构造函数。辨识重载:靠参数而不是返回值的不同来区分重载函数,编译器根据参数为每个重载函数产生不同的内部标识符号。
    extern "C"的用处,如果C++程序要调用已经被编译后的C函数,假设谋个 C函数的声明如下:
void foo(int x,int y)
//该函数被C编译器编译后在库中的名字为 _foo,而C++编译器则会产生像 _foo_int_int之类的名字用来支持函数重载和类型检查
//由于编译后的名字不同,C++程序不能直接调用C函数,因此,c++提高了一个C连接交换指定符合 extern “C”来解决这个问题。
extern “C”
{
 void foo(int x, int y);// 其它函数
}
或者写成
extern “C”
{
 #include “myheader.h”// 其它C 头文件
}

注意并不是两个函数的名字相同就能构成重载,全局函数和类的成员函数同名不算重载,因为函数的作用域不同。例如

void Print(); // 全局函数
class A
{void Print(); // 成员函数
}
//全局函数的调用
::Print(i)
  1. 覆盖
    成员函数被重载:1,相同的范围,在同一个类中;2,函数名字相同;
    3,参数不同;4,virtual关键字可有可恶。覆盖指派生类函数覆盖基类函数:1,不同的范围;2,函数名字相同;3,参数相同;4,基类必须有关键字 virtual
class Base
{
 public:
  void f(int x){ cout << "Base::f(int) " << x << endl; }
  void f(float x){ cout << "Base::f(float) " << x << endl; }
  virtual void g(void){ cout << "Base::g(void)" << endl;}
};
class Derived : public Base
{
 public:
  virtual void g(void){ cout << "Derived::g(void)" << endl;}
};
void main(void)
{
 Derived d;
 Base *pb = &d;
 pb->f(42); // Base::f(int) 42
 pb->f(3.14f); // Base::f(float) 3.14
 pb->g(); // Derived::g(void)
}
  1. 隐藏
    派生类的函数屏蔽了与其同名的基类函数;1,如果派生类的函数与基类的函数同名,但是参数不同,此时,不论有没virtual,基类函数将被隐藏:区别重载;2,如果派生类函数与基类的函数同名,并且参数也显然,但是基类没有virtual关键字,此时,基类函数被隐藏;区别覆盖
class Base
{
public:
 virtual void f(float x){ cout << "Base::f(float) " << x << endl; }
 void g(float x){ cout << "Base::g(float) " << x << endl; }
 void h(float x){ cout << "Base::h(float) " << x << endl; }
};
class Derived : public Base
{
public:
    //覆盖了基类f(float)
 virtual void f(float x){ cout << "Derived::f(float) " << x << endl; }
 //隐藏了基类g(float)
 void g(int x){ cout << "Derived::g(int) " << x << endl; }
 //隐藏了基类h(float)
 void h(float x){ cout << "Derived::h(float) " << x << endl; }
};
//测试
void main(void)
{
    Derived d;
    Base *pb = &d;
    Derived *pd = &d;
    // Good : behavior depends solely on type of the object
    pb->f(3.14f); // Derived::f(float) 3.14
    pd->f(3.14f); // Derived::f(float) 3.14
    // Bad : behavior depends on type of the pointer
    //隐藏依赖指针类型
    pb->g(3.14f); // Base::g(float) 3.14
    pd->g(3.14f); // Derived::g(int) 3 (surprise!)
    // Bad : behavior depends on type of the pointer
    pb->h(3.14f); // Base::h(float) 3.14 (surprise!)
    pd->h(3.14f); // Derived::h(float) 3.14
}
// 隐藏带来的麻烦和改造
class Base
{
public:
 void f(int x);
};
class Derived : public Base
{
public:
 void f(char *str);
};
void Test(void)
{
    Derived *pd = new Derived;
    pd->f(10); // error
}
//改造
class Derived : public Base
{
public:
    void f(char *str);
    void f(int x) { Base::f(x); }
};
  1. 参数的缺省值
    有一些参数在每次函数调用时都相同,因此用缺省值变得简洁。1,参数缺省值只能出现在函数的声明中,而不能出现在定义体内.为什么?一是函数的定义本来就与参数是否有缺省值无关,所以没有必要让缺省值出现在函数的定义体内;二是参数的缺省值可能还会改动,显然修改函数的声明比修改函数的定义要方便。2,如果函数有多个参数,参数只能从后向前挨个缺省;正确的示例如下:void Foo(int x, int y=0, int z=0);错误的示例如下:void Foo(int x=0, int y, int z=0);
void Foo(int x=0, int y=0); // 正确,缺省值出现在函数的声明中
void Foo(int x=0, int y=0) // 错误,缺省值出现在函数的定义体中
{}
  1. 运算符重载
    关键字 operator加上运算符来表示函数。
    Complex operator +(const Complex &a, const Complex &b);

  2. 函数内联 inline,目的是提高函数的执行效率。C程序中,可以用宏代码提高执行效率,宏代码本身不是函数,但使用起来像函数,预处理器用复制宏代码的方式替代函数调用,省去了参数压栈,生成汇编语言的CALL调用,返回参数,执行return等过程,从而提高了速度。
    C++的函数内联是如何工作的?内联函数,编译器在符号表里放入函数的声明,包括名字,参数类型和返回值类型,如果编译器没有发现内联函数存在错误,那么该函数的代码也被放入符号表里,在调用一个内联函数时,编译器首先检查调用是否正确,进行类型安全检查,或者进行自动类型转换,如果正确,内联函数的代码就会直接替换函数的调用,于是省去了函数调用的开销。假设内联函数 是成员函数,对象的地址 this会被放在合适的地方,这是预处理器办不到的。与预处理不同?预处理器不能进行类型安全检查,或者进行自动类型转换。1,关键字inline必须与函数定义体放在一起才能使函数成为内联,仅与声明一起不起作用

inline void Foo(int x, int y); // inline 仅与函数声明放在一起
void Foo(int x, int y)
{}
//才是内联
void Foo(int x, int y);
inline void Foo(int x, int y) // inline 与函数定义体放在一起
{}

2,定义在类声明之中的成员函数将自动地成为内联函数

class A
{
public:
 void Foo(int x, int y) {} // 自动地成为内联函数
}
//但以上不是良好编程风格,改为
// 头文件
class A
{
public:
 void Foo(int x, int y)}
// 定义文件
inline void A::Foo(int x, int y)
{}

慎用内联 1,内联是以代码膨胀复制为代价,仅仅省去了函数调用地开销,但是如果函数体内执行代码时间相比于函数调用开销大,将没有收益;2,函数体内的代码比较长,使用内联将导致内存消耗代价较高;3,函数体内出现循环,那么执行函数体内代码的时间要比函数调用开销大;4,不要随便地将构造函数和析构函数的定义体放在类声明中,默认为inline。

  1. 类的设计
    1.构造函数析构函数与赋值函数
    每个类只有一个析构函数和一个赋值函数,但是可以有多个构造函数,包含一个拷贝构造函数,其他的成为普通构造函数。对于任意一个类A,如果不想编写上述函数,C++编译器将自动为A产生四个缺省的函数。
A(void); // 缺省的无参数构造函数
A(const A &a); // 缺省的拷贝构造函数 位拷贝
~A(void); // 缺省的析构函数
A & operate =(const A &a); // 缺省的赋值函数 值拷贝

class String
{
public:
    String(const char *str = NULL); // 普通构造函数
    String(const String &other); // 拷贝构造函数
    ~ String(void); // 析构函数
    String & operate =(const String &other); // 赋值函数
private:
 char *m_data; // 用于保存字符串
};

为什么会有拷贝和析构函数?
根据经验,不少难以察觉的程序错误是由于变量没有被正确初始化或清除造成,因此把对象的初始化放在构造函数,把清除工作放在析构函数,当对象被创建时候,构造自动执行,对象消亡时,析构自动执行,不要担心忘记对象的初始化和清除工作了。
2. 构造函数的初始化列表
初始化列表工作发生在函数体内的任何代码被执行之前
1,如果类存在继承关系,派生类必须在其初始化列表里调用基类的构造函数

class A
{A(int x); // A 的构造函数
};
class B : public A
{B(int x, int y);// B 的构造函数
};
B::B(int x, int y)
: A(x) // 在初始化表里调用A 的构造函数
{}

2,类的const常量只能在初始化列表里被初始化,因为它不能在函数体内赋值的方式来初始化

3,非内部数据类型的成员对象应当采用初始化列表方式来初始化,以获取更高的效率。

class A
{A(void); // 无参数构造函数
    A(const A &other); // 拷贝构造函数
    A & operate =( const A &other); // 赋值函数
}class B
{
public:
    B(const A &a); // B 的构造函数
private:
 A m_a; // 成员对象
};
//初始化列表
B::B(const A &a)
: m_a(a)
{}
//函数体内赋值
//先暗地里创建 m_a对象,调用A的无参数构造函数,再调用A的赋值函数,将参数a赋给m_a
B::B(const A &a)
{
 m_a = a;}

注意:成员对象初始化的次序完全不受他们再初始化表中的次序的影响,只由成员对象在类中声明的次序决定,因为类的声明是唯一的,而类的构造函数可以有多个,因此会有不同次序的初始化表。如果成员对象按照初始化表的次序进行构造,将导致析构函数无法得到唯一的逆序.

  1. 分析String的构造与析构
    1,string的普通构造
// String 的普通构造函数
String::String(const char *str)
{
    if(str==NULL)
    {
        m_data = new char[1];
        *m_data = ‘\0;
    }
    else
    {
        int length = strlen(str);
        m_data = new char[length+1];
        strcpy(m_data, str);
    }
}
// String 的析构函数
String::~String(void)
{
    delete [] m_data;
    // 由于m_data 是内部数据类型,也可以写成 delete m_data;
}

2,string的拷贝构造

// 拷贝构造函数
String::String(const String &other)
{
    // 允许操作other 的私有成员m_data
    int length = strlen(other.m_data);
    m_data = new char[length+1];
    strcpy(m_data, other.m_data);
}
// 赋值函数
String & String::operate =(const String &other)
{
    // (1) 检查自赋值
    if(this == &other)
     return *this;
    // (2) 释放原有的内存资源,不释放会造成内存泄漏
    delete [] m_data;
    // (3)分配新的内存资源,并复制内容
    int length = strlen(other.m_data);
    m_data = new char[length+1];
    strcpy(m_data, other.m_data);
    // (4)返回本对象的引用
    return *this;
}

引用不可能为NULL,但是指针可以为NULL,不能 return other,因为可能other是个临时对象,在赋值结束后它马上消失,那么return other返回的将是垃圾。

  1. 派生类中实现类的基本函数
    1,派生类的构造函数应在其初始化列表调用基类的构造函数
    2,基类与派生类的析构函数应该设置virtual
class Base
{
public:
 virtual ~Base() { cout<< "~Base" << endl ; }
};
class Derived : public Base
{
public:
 virtual ~Derived() { cout<< "~Derived" << endl ; }
};
void main(void)
{
    Base * pB = new Derived; // upcast
    delete pB;
}

输出结果为: ~Derived ~Base 如果析构函数不为虚,那么输出结果为 ~Base

3,编写派生类的赋值函数时,注意不要忘记对基类的数据成员进行重新赋值

class Base
{
public:
…
    Base & operate =(const Base &other); // 类Base 的赋值函数
private:
    int m_i, m_j, m_k;
};
class Derived : public Base
{
public:
…

    Derived & operate =(const Derived &other); // 类Derived 的赋值函数
private:
 int m_x, m_y, m_z;
};
Derived & Derived::operate =(const Derived &other)
{
    //(1)检查自赋值
    if(this == &other)
     return *this;
    //(2)对基类的数据成员重新赋值
    Base::operate =(other); // 因为不能直接操作私有数据成员
    //(3)对派生类的数据成员赋值
    m_x = other.m_x;
    m_y = other.m_y;
    m_z = other.m_z;
    //(4)返回本对象的引用
    return *this;
}
  1. const成员函数
    const更大的魅力是它可以修饰函数的参数,返回值,甚至函数的定义体。
    函数参数:
    1,如果输入参数采用指针传递,那么加 const防止意外改动该指针,起到保护作用,void StringCopy(char *strDestination, const char *strSource);
    2,如果采用值传递,由于函数将自动产生临时变量用于复制该参数,该输入参数本来就无需保护,不要加const
    3,对于非内部数据类型的参数,void Func(A a)这样声明的函数注定效率比价低,因为函数体内将产生A类型的临时对象用来复制参数a,而临时对象的构造、复制,析构都将消耗时间。 void Func(const A &a) 这样仅仅借用一下参数别名,不需要产生临时对象,提高效率。
    4,对于内部数据类型没有必要写成 void Func(const int &X),因为内部数据类型参数不存在构造,析构过程, 函数返回值
    5,如果给以 指针传递方式的函数返回值加 const修饰,那么函数返回值 指针的内容不能被修改,该返回值只能被赋给加 const修饰的同类型指针,例如函数const char * GetString(void);如下语句将出现编译错误:char *str = GetString();正确的用法是const char *str = GetString();
    6,如果函数返回值采用值传递方式,由于函数会把返回值复制到外部临时的存储单元中,加 const修饰没有任何价值,例如不要把函数int GetInt(void) 写成const int GetInt(void)
    7,函数返回值采用引用传递的场合并不多,这样方式一般只出现在类的赋值函数中,目的是为了实现链式传递
class A
{⋯
 A & operate = (const A &other); // 赋值函数
};
A a, b, c; // a, b, c 为A 的对象
⋯
a = b = c; // 正常的链式赋值
(a = b) = c; // 不正常的链式赋值,但合法

8,任何不会修改数据成员的函数都应该声明为const类型,如果在编写const成员函数时,不慎修改了数据成员,或者调用了其他非const成员函数,编译器将报错,提高程序的健壮性。

class Stack
{
public:
    void Push(int elem);
    int Pop(void);
    int GetCount(void) const; // const 成员函数
private:
    int m_num;
    int m_data[100];
};
int Stack::GetCount(void) const
{
    ++ m_num; // 编译错误,企图修改数据成员m_num
    Pop(); // 编译错误,企图调用非const 函数
    return m_num;
}
CPP程序 #include "stdio.h" #include #include #define getpch(type) (type*)malloc(sizeof(type)) #define NULL 0 struct pcb { /* 定义进程控制块PCB */ char name[10]; char state; int super; int ntime; int rtime; struct pcb* link; }*ready=NULL,*p; typedef struct pcb PCB; sort() /* 建立对进程进行优先级排列函数*/ { PCB *first, *second; int insert=0; if((ready==NULL)||((p->super)>(ready->super))) /*优先级最大者,插入队首*/ { p->link=ready; ready=p; } else /* 进程比较优先级,插入适当的位置中*/ { first=ready; second=first->link; while(second!=NULL) { if((p->super)>(second->super)) /*若插入进程比当前进程优先数大,*/ { /*插入到当前进程前面*/ p->link=second; first->link=p; second=NULL; insert=1; } else /* 插入进程优先数最低,则插入到队尾*/ { first=first->link; second=second->link; } } if(insert==0) first->link=p; } } input() /* 建立进程控制块函数*/ { int i,num; //clrscr(); /*清屏*/ printf("\n 请输入进程数?"); scanf("%d",&num); for(i=0;iname); printf("\n 输入进程优先数:"); scanf("%d",&p->super); printf("\n 输入进程运行时间:"); scanf("%d",&p->ntime); printf("\n"); p->rtime=0;p->state='w'; p->link=NULL; sort(); /* 调用sort函数*/ } } int space() { int l=0; PCB* pr=ready; while(pr!=NULL) { l++; pr=pr->link; } return(l); } disp(PCB * pr) /*建立进程显示函数,用于显示当前进程*/ { printf("\n qname \t state \t super \t ndtime \t runtime \n"); printf("|%s\t",pr->name); printf("|%c\t",pr->state); printf("|%d\t",pr->super); printf("|%d\t",pr->ntime); printf("|%d\t",pr->rtime); printf("\n"); } check() /* 建立进程查看函数 */ { PCB* pr; printf("\n **** 当前正在运行的进程是:%s",p->name); /*显示当前运行进程*/ disp(p); pr=ready; printf("\n ****当前就绪队列状态为:\n"); /*显示就绪队列状态*/ while(pr!=NULL) { disp(pr); pr=pr->link; } } destroy() /*建立进程撤消函数(进程运行结束,撤消进程)*/ { printf("\n 进程 [%s] 已完成.\n",p->name); free(p); } running() /* 建立进程就绪函数(进程运行时间到,置就绪状态*/ { (p->rtime)++; if(p->rtime==p->ntime) destroy(); /* 调用destroy函数*/ else { (p->super)--; p->state='w'; sort(); /*调用sort函数*/ } } main() /*主函数*/ { int len,h=0; char ch; input(); len=space(); while((len!=0)&&(ready!=NULL)) { ch=getchar(); h++; printf("\n The execute number:%d \n",h); p=ready; ready=p->link; p->link=NULL; p->state='R'; check(); running(); printf("\n 按任一键继续......"); ch=getchar(); } printf("\n\n 进程已经完成.\n"); ch=getchar(); }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值