万字长文【C++】高质量编程指南

2022-06-16 16:58:35 浏览数 (1)

8.高质量编程

8.1.基础知识

1,应用 ifndef/define/endif结构产生预处理块的目的是:防止头文件被重复引用。

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

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

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

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

6,if语句

(1)不可将布尔变量直接与true,false或者1,0进行比较

根据布尔类型的语义,零值为"假" (false),任何非零值都是“真”(true),true的值究竟是什么并没有统一的标准。

代码语言:javascript复制
//bool flag
if (flag)//真
if (!flag)//假
//不良风格
if (flag == true)
if (flag == 0)

(2)应当将整型变量用 == 或 != 直接与 0 比较

代码语言:javascript复制
//int value
if (value == 0)
//不良风格 误解是bool
if (!value)

(3)不可将浮点变量用 == 或 != 与任何数字比较

无论是float还是double类型的变量,都有精度限制,所以一定要避免将浮点类型变量用 == 与数字比较,应该设法转换成 >= 或 <= 形式

代码语言:javascript复制
//float x
if (x >= erp)
//不良风格
if (x == 0.0)

(4)指针变量直接与 NULL比较,而不是与 0比较

尽管NULL的值与0相同,但是两者的意义不同,回答 if (NULL == p) 和 if (p == NULL)的区别?

因为NULL不能被赋值,如果漏写成 NULL = p,编译器会直接报错的,但是 如果漏写成 p = NULL,不会报错,if语句的含义就变味道了!

7,for循环语句

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

代码语言:javascript复制
//低效率
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)如果循环体内存在逻辑判断,并且循次数很大,将逻辑判断移到循环体的外面。,如果在内部,就会多执行很多次逻辑判断,并且破坏了循环“流水线”作业,使得编译器不能对循环进行优化处理,降低了效率。

代码语言:javascript复制
//效率低
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语句的循环控制变量的取值采用 半开闭区间的写法

代码语言:javascript复制
//半开半闭
for (int i = 0; i < N;i  )
//闭区间 不建议
for (int i = 0 ; i <= N -1; i  )

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

代码语言:javascript复制
{
 {
  {
   goto error;
  }
 }
}

9,const常量有数据类型,而#define没有数据类型,编译器可以对前者进行类型安全检查,而只对后者进行字符替换,没有类型安全检查。const常量完全可以取代宏常量。

(1)不能再类声明中初始化 const数据成员,因为类的对象未被创建时,编译器不知道 SIZE的值是多少。

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

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

代码语言:javascript复制
class A
{
 A(int size);
 const int SIZE;
};
A::A(int size):SIZE(size)
{

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

(3)通过类中的枚举常量实现在整个类中都恒定的常量,不用指望const数据成员了。

枚举常量不会占用对象的存储空间,它们在编译时被全部求值,其缺点是:它的隐含数据类型是整数,其最大值有限,且不能表示浮点数。

代码语言:javascript复制
class A
{
 enum{SIZE =100, SIZE2 =200};
 int array1[SIZE];
 int arrray2[SIZE2];
};

10,函数设计

(1)如果函数没有参数,则用 void填充。

代码语言:javascript复制
float GetValue(void);//良好风格
float GetValue();//不良风格

(2)目的参数放在前面,源参数放在后面

(3)如果参数是指针,且仅作输入用,则应在类型前加 const,以防止该指针在函数体内被意外修改。

代码语言:javascript复制
void StringCopy(char *Des, const char *str)

(4)如果输入的参数以值传递的方式传递对象,则改用 const & 方式来传递,这样可以省去临时对象的构造和析构过程,从而提高效率。

(5)如果函数的返回值是一个对象,有些场合用引用传递替换值传递可以提高效率,而有些场合只能用值传递而不能用引用传递,否则会出错。

代码语言:javascript复制
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 对象,如果用值传递的方式,虽然功能仍然正确,但由于 retuern 语句要把 *this 拷贝到保存返回值的外部存储单元之中,增加了不必要的开销,降低了赋值函数的效率。

代码语言:javascript复制
String a,b,c;
//如果用值传递,将产生一次 *this的拷贝
a =b;
//将产生两次的 *this拷贝
a=b=c;

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

代码语言:javascript复制
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在函数结束时被自动销毁,将导致返回的引用无效。

代码语言:javascript复制
c =a  b;//此时a b并不返回期望值,c什么也得不到,留下隐患

(6)return语句不可返回指向 栈内存 的指针或者引用,因为该内存在函数体结束时被自动销毁

代码语言:javascript复制
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在函数结束时被销毁,调用析构函数

类似地:

return int(x y)

不要写成:

int temp =x y;

return 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。

8.2.内存管理

8.2.1.内存的分配方式

1,静态存储区域分配:内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在,例如全局变量,static变量

2,在栈上创建:在执行函数时候,函数内部局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。

3,堆内存:动态内存分配,程序在运行时候用malloc或new申请任意多少的内存,程序员自己负责在何时用 free或delete释放内存,动态内存的生存期由我们决定,使用灵活,但是问题也多。

8.2.2.内存使用错误

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,导致产生了 野指针

8.2.3.指针与数组对比

数组要么在静态存储区被创建,如全局数组,要么在栈上被创建。数组名对应着一块内存,其地址与容量在生命期内保持不变,只有数组的内容可以改变。

指针可以随时指向任意类型的内存块,它的特征是 可变,所以我们常用指针来操作动态内存,指针远比数组灵活,但也更危险。

字符串比较指针与数组

1,修改内容

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

2,内容复制与比较

不能对数组名进行直接复制与比较,否则会产生编译错误;

指针 p =a 并不能把 a的内容复制指针 p,而是把 a的地址赋给了p。要想复制 a的内容,可以先用库函数 malloc为p申请一块容量 为 strlen(a) 1个字符的内存,再用strcpy进行字符串复制。

代码语言:javascript复制
//数组
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)

3,计算内存容量

sizeof计算数组的容量(字节数),并忘记 ""。

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

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

代码语言:javascript复制
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释放。

2,可以用 指向指针的指针 实现用指针参数去申请内存

代码语言:javascript复制
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);
}

3,继续简化为函数返回值来传递动态内存

代码语言:javascript复制
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);
}

4,不要用 return语句返回指向 栈内存的指针,因为该内存在函数结束时自动消亡

代码语言:javascript复制
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);
}
8.2.5.free和delete把指针怎么了

它们只是把指针所指的内存给释放掉,当并没有把指针本身干掉。

代码语言:javascript复制
char *p =(char*)malloc(100);
strcpy(p,”hello);
//p所指向的内存被释放,但是p所指的地址仍然不变
//不是NULL,只是该地址对应的内存是垃圾,p成了野指针
free(p);

.....
if(p != NULL)//没有起到防错作用
{
 //野指针,出错
 strcpy(p,"hello");
}

8.2.6.杜绝野指针

野指针不是NULL指针,是指向 垃圾内存的指针,野指针很危险,if拦不住,成为野指针的原因有:

1,指针变量没有被初始化

指针变量在创建的同时应当被初始化,要么将指针设置为 NULL,要么让它指向合法的内存。

代码语言:javascript复制
char *p = NULL:
cahr *str = (char*)malloc(100);

2,指针 p被free或者delete之后,没有设为NULL

3,指针操作超越了变量的作用范围

代码语言:javascript复制
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 是“野指针”
}
8.2.7.malloc/free和new/delete的区别

malloc/free是 C /C语言标准的库函数,对于非内部数据类型的对象而言,它无法满足动态对象的要求,对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数,库函数不在编译器控制的权限之内,不能执行构造和析构。

new/delete是C 运算符,能完成动态内存分配和初始化工作,如何实现动态内存管理。

代码语言:javascript复制
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申请的动态内存,理论上程序不会出错,但是可读性差,因此,必须成对出现。

8.3.函数的高级特性

重载 overloaded,内联 inline:既可以用于全局函数也可用于类的成员函数;

const和virtual仅仅用于类的成员函数。

8.3.1.重载

语义,功能相似的几个函数用同一个名字表示,提高函数的易用性。并且,类的构造函数需要重载,C 规定构造函数于类同名,只有一个名字,如果想用几种不同的方法创建对象,只能用重载机制来实现,所以类可以有多个同名的构造函数。

辨识重载:靠参数而不是返回值的不同来区分重载函数,编译器根据参数为每个重载函数产生不同的内部标识符号。

extern "C"的用处

如果C 程序要调用已经被编译后的C函数,我?

假设谋个 C函数的声明如下:void foo(int x,int y)

代码语言:javascript复制
//该函数被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 头文件
}

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

代码语言:javascript复制
void Print(⋯); // 全局函数
class A
{⋯
 void Print(⋯); // 成员函数
}
//全局函数的调用
::Print(i)
8.3.2.覆盖

成员函数被重载:

1,相同的范围,在同一个类中

2,函数名字相同

3,参数不同

4,virtual关键字可有可恶

覆盖指派生类函数覆盖基类函数

1,不同的范围

2,函数名字相同

3,参数相同

4,基类必须有关键字 virtual

代码语言:javascript复制
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)
}
8.3.3.隐藏

派生类的函数屏蔽了与其同名的基类函数

1,如果派生类的函数与基类的函数同名,但是参数不同,此时,不论有没virtual,基类函数将被隐藏:区别重载

2,如果派生类函数与基类的函数同名,并且参数也显然,但是基类没有virtual关键字,此时,基类函数被隐藏;区别覆盖

代码语言:javascript复制
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
}

隐藏带来的麻烦和改造

代码语言:javascript复制
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); }
};
8.3.4.参数的缺省值

有一些参数在每次函数调用时都相同,因此用缺省值变得简洁。

1,参数缺省值只能出现在函数的声明中,而不能出现在定义体内

代码语言:javascript复制
void Foo(int x=0, int y=0); // 正确,缺省值出现在函数的声明中
void Foo(int x=0, int y=0) // 错误,缺省值出现在函数的定义体中
{
⋯
}

为什么?一是函数的定义本来就与参数是否有缺省值无关,所以没有必要让缺省值出现在函数的定义体内;二是参数的缺省值可能还会改动,显然修改函数的声明比修改函数的定义要方便。

2,如果函数有多个参数,参数只能从后向前挨个缺省

代码语言:javascript复制
正确的示例如下:
void Foo(int x, int y=0, int z=0);

错误的示例如下:
void Foo(int x=0, int y, int z=0);
8.3.5.运算符重载

关键字 operator加上运算符来表示函数。

Complex operator (const Complex &a, const Complex &b);

8.3.6.函数内联

inline,目的是提高函数的执行效率。

C程序中,可以用宏代码提高执行效率,宏代码本身不是函数,但使用起来像函数,预处理器用复制宏代码的方式替代函数调用,省去了参数压栈,生成汇编语言的CALL调用,返回参数,执行return等过程,从而提高了速度。

C 的函数内联是如何工作的?

内联函数,编译器在符号表里放入函数的声明,包括名字,参数类型和返回值类型,如果编译器没有发现内联函数存在错误,那么该函数的代码也被放入符号表里,在调用一个内联函数时,编译器首先检查调用是否正确,进行类型安全检查,或者进行自动类型转换,如果正确,内联函数的代码就会直接替换函数的调用,于是省去了函数调用的开销。假设内联函数 是成员函数,对象的地址 this会被放在合适的地方,这是预处理器办不到的。

与预处理不同?

预处理器不能进行类型安全检查,或者进行自动类型转换。

1,关键字inline必须与函数定义体放在一起才能使函数成为内联,仅与声明一起不起作用

代码语言:javascript复制
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,定义在类声明之中的成员函数将自动地成为内联函数

代码语言:javascript复制
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。

8.4.类的设计

8.4.1.构造函数析构函数与赋值函数

每个类只有一个析构函数和一个赋值函数,但是可以有多个构造函数,包含一个拷贝构造函数,其他的成为普通构造函数。

对于任意一个类A,如果不想编写上述函数,C 编译器将自动为A产生四个缺省的函数。

代码语言:javascript复制
A(void); // 缺省的无参数构造函数
A(const A &a); // 缺省的拷贝构造函数 位拷贝
~A(void); // 缺省的析构函数
A & operate =(const A &a); // 缺省的赋值函数 值拷贝
代码语言:javascript复制
class String
{
public:
    String(const char *str = NULL); // 普通构造函数
    String(const String &other); // 拷贝构造函数
    ~ String(void); // 析构函数
    String & operate =(const String &other); // 赋值函数
private:
 char *m_data; // 用于保存字符串
};

为什么会有拷贝和析构函数?

根据经验,不少难以察觉的程序错误是由于变量没有被正确初始化或清除造成,因此把对象的初始化放在构造函数,把清除工作放在析构函数,当对象被创建时候,构造自动执行,对象消亡时,析构自动执行,不要担心忘记对象的初始化和清除工作了。

8.4.2.构造函数的初始化列表

初始化列表工作发生在函数体内的任何代码被执行之前

1,如果类存在继承关系,派生类必须在其初始化列表里调用基类的构造函数

代码语言:javascript复制
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,非内部数据类型的成员对象应当采用初始化列表方式来初始化,以获取更高的效率。

代码语言:javascript复制
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;
…
}

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

8.4.3.分析String的构造与析构

1,string的普通构造

代码语言:javascript复制
// String 的普通构造函数
String::String(const char *str)
{
    if(str==NULL)
    {
        m_data = new char[1];
        *m_data = ‘’;
    }
    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的拷贝构造

代码语言:javascript复制
// 拷贝构造函数
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返回的将是垃圾。

8.4.4.派生类中实现类的基本函数

1,派生类的构造函数应在其初始化列表调用基类的构造函数

2,基类与派生类的析构函数应该设置virtual

代码语言:javascript复制
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,在编写派生类的赋值函数时,注意不要忘记对基类的数据成员进行重新赋值

代码语言:javascript复制
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;
}
8.4.4.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修饰的同类型指针

代码语言:javascript复制
例如函数
const char * GetString(void);
如下语句将出现编译错误:
char *str = GetString();
正确的用法是
const char *str = GetString();

6,如果函数返回值采用值传递方式,由于函数会把返回值复制到外部临时的存储单元中,加 const修饰没有任何价值

代码语言:javascript复制
例如不要把函数int GetInt(void) 写成const int GetInt(void)

7,函数返回值采用引用传递的场合并不多,这样方式一般只出现在类的赋值函数中,目的是为了实现链式传递

代码语言:javascript复制
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成员函数,编译器将报错,提高程序的健壮性。

代码语言:javascript复制
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;
}

注:来源 高质量C /C 编程指南 书籍整理

作者:林锐博士

学习使用,如有侵权,联系立删!

认识一个人就是开了一扇窗户,就能看到不一样的东西,听到不一样的声音,能让你思考,觉悟,这已经够了。其他还有很多,比如机会,帮助,我不确定。这个在一般人看来可能不重要,但是我知道这个很重要。

我是小阳哥

希望用身边的人,身边的事

让我们少走一些弯路,一点点就好

0 人点赞