C++进阶之高质量代码编程

C++高质量代码

在之前已经讲过C++的基础,其实许多编程语言的入门大差不差,无外乎数据类型,数据结构,循环语句,条件语句,其实真正区别的是用途,已经语言的特性,这个才是最为重要的,不过在这之前,一个良好的代码风格是必不可少的,所以这里介绍一篇自己总结的《C++高质量代码》的笔记。

1.文件结构

1.1 版权和版本的声明

声明位于头文件和定位文件的开头(这里的定义文件可以是函数)

/*
* Copyright (c) time,方世杰
* All right reserved.
*
* 文件名称:
* 文件标识:
* 摘   要:
*
* 当前版本:1.1
* 作   者:author
* 完成日期:2022.5.20
*
* 取代版本:1.0
* 原作者 :author
* 完成日期:2022.5.20
*/
'''
* Copyright (c) time,方世杰
* All right reserved.
*
* 文件名称:
* 文件标识:
* 摘   要:
*
* 当前版本:1.1
* 作   者:author
* 完成日期:2022.5.20
*
* 取代版本:1.0
* 原作者 :author
* 完成日期:2022.5.20
'''

1.2 头文件的作用

  • 通过头文件来调用库功能,能够在很多场合很好的保存源代码,只需要向用户提供头文件和二进制的库即可

  • 建议将函数的定义与声明分开,无论函数体有多小

  • <>头文件格式来引用标准库的头文件(编译器将从标准库目录开始搜索)

  • “”头文件格式来引用非标准库的头文件(编译器将从用户的工作目录开始搜索)

1.3 目录结构

  • include保存头文件

  • source保存定义文件,可以是多级目录

2.程序的板式

2.1 空行

  • 每个类声明之后,每个函数定义结束后都需要加空行。

  • 在一个函数体内,逻辑上密切相关的语句之间不加空行,其他地方加空行分隔。

2.2 代码行

  • 一行代码只定义一个变量,或只写一条语句。
  • 尽可能在定义变量的同时初始化该变量

2.3 代码行内的空格

  • ,或者;贴近前一条语句,表示是前一条语句的结束。
  • 二元操作符前后两行都要加空行
  • 一元操作符要贴近被操作的对象

2.4 对齐

  • {}需要分别独占一行
  • {}需要左括号和右括号对齐

2.5 长行拆分

  • 一般代码行的最大长度在70-80个字符以内
  • 如果太长,常在操作符处进行换行处理

2.6 修饰符的位置

  • 这里强调指针*,一般用在变量身边,这样便于区分。

2.7 注释

  • 注释是提示,不是文档
  • 保持代码与注释的一致性
  • 准确,易懂,防止注释具有二义性
  • 多重嵌套

2.8 类的板式

  • 以数据为中心:private放在public的前面。
  • 以行为为中心:public放在private的前面。

3.命名规则

//文件命名,可以包含下划线(_)或者连字符(-),注意C++文件一般用,cc结尾,专门插入文本的文件以.inc结尾
//类名,大驼峰命名法
//命名空间命名,以小写字母命名

//普通变量,采用下划线命名规范
//成员变量,采用下划线,但是最后结尾需要额外添加_
//全局变量,前面加上g_
//静态变量,前面加上s_
//常量,以const等命名的变量,采用k+大驼峰命名法
//结构体成员变量:和普通变量相同即可
//枚举命名,和常量或宏一致,可以k+大驼峰命名 或者 全大写
//宏命名,全大写,可以接下划线,但是通常不建议使用,这一点在后面会谈到

//函数名,大驼峰命名法
//取值和设置函数命名:与成员变量命名差不多,采用下划线
  • 变量采用名词,或者形容词+名词
  • 函数采用动词,或者动词+名词

这里参考谷歌的命名规范

4.表达式和基本语句

4.1 复合表达式

a=b=c=0,书写整洁,可以提高编译效率

4.2 if语句

//bool
if(flag){}

//int
if(0 == a){}

//float
if(abs(a) < EPS){}
/*这里的EPS是一个误差,表示允许的误差,可以直接设置*/

//char *
if(NULL == a){}

4.3 循环语句的效率

  • 将最长的循环放到最内侧,减少CPU跨切循环层的次数
  • 循环体内的执行语句越少越好,比如逻辑判断语句放在外面,代码上的冗余不代表执行效率上的低下,有时候反而会更好。

4.4 for语句循环控制变量

  • 采用左闭右开的方式,更加直观,尽管两者的功能是相同的。

4.5 switch语句

面对多分支选择,采用switch书写效果会更好,且还可以通过break的去留,实现一些“华丽的操作”。

4.6 goto语句

他会让原本结构化的设计变的糟糕,但是同样它也有适合的场所,比如他能从多重循环体重咻地一下跳到外面,用不着一层一层的使用break语句。

5 常量

5.1 const与#define比较

const相较于#define来说,多了安全检查,#define可能出现意向不到的边际效应。

这里的边际效应是指你的宏定义比如a+b没有加括号,在使用宏时,出现意想不到的错误。

5.2 类中的常量

  • 不要在类声明中初始化const数据成员,需要在构造函数中进行初始化

    class A
    
    {
    
        const int size=0;
    
    }
    // 上述声明是错误的,不能在类的声明里初始化常量,除非加上static,或者在构造函数中进行初始化
    class A
    
    {
    
        static const int size=0;//(属于类内初始化)
    
    }
    
    或者
    
    class A
    
    {
    
        A() {const int size=0;}//(属于构造函数中初始化)
    
    }
    class A
    {
        A(int size);
        const int SIZE;
    }
    A::A(int size):SIZE(size)
    

    这里建议不要在类的声明中去定义某些东西,最好在构造函数中进行初始化

  • 如果想要整个类中都恒定的常量,可以采用类的枚举变量来实现,但是枚举常量只能包含整数。

    但是有时候我们需要在类中定义有一定容量的数组,我们该怎么办呢,可以采用枚举

6 函数的设计

6.1 参数的规则

  • 不建议省略参数的名字,建议填写完成

  • 目的函数放在前面,源函数放在后面

  • 多采用const,来表示仅输入的变量,可以减小代码出错的概率

  • 多采用&的方式,可以节省临时对象的构造和析构过程,从而提高效率

  • 参数不要太多,控制在5个以内。

6.2 返回值的规则

  • 为了支持链式表达式,可以附加返回值。

  • string中的data成员,是char类型

  • return不能返回“栈内存”的“指针”或者“引用”,因为该内存在函数体结束之后会被自动销毁。

  • return int(x+y)这种形式,可以省去不必要的构造函数和析构函数的时间,提高效率。

  • 少使用static局部变量,你把握不住

6.3 断言

仅仅在Debug版本起作用的宏,可以用于检查“不应该”发生的情况。这里你需要区分非法情况和错误情况之间的区别。

之后会专门开一期介绍断言

6.4 引用与指针的比较

  • 引用被创建时必须被初始化
  • 不能有NULL的引用,引用必须与合法的存储单元关联。

7 内存管理

7.1 内存分配方式

  • 从静态存储区域分配,例如全局变量,static变量
  • 栈上创建,执行函数时,函数内部局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放,效率高,但是分配内存有限。
  • 从堆上分配,动态内存分配,使用malloc或new申请内存,程序员自己通过free或delete释放内存。

7.2 常见内存错误及对策

  • 内存分配未成功,却使用了它

解决方法:提前检查指针是否为NULL

  • 内存分配虽然成功,但是尚未初始化就引用它

解决方法:一定要赋初值,哪怕是为0

  • 内存分配成功并且已经初始化,但操作越过了内存的边界。

问题原因:多是由于数组索引下标多1或者少1操作,特别是for循环

  • 忘记释放内存导致内存泄露,系统提示内存耗尽

解决方法:申请和释放必须一一对应

  • 释放了内存,却继续使用它
  1. 对象调用过于复杂,难以搞清楚某个对象究竟是否已经释放了内存,此时应该重新设计数据结构。
  2. 函数的return写错了,返回了“栈内存”的“指针”或者“引用”。
  3. free或者delete之后,没有将指针设置为NULL

7.3 指针与数组的比较

7.3.1 修改内容

  • 指针指向常量字符串,是不可以被修改的
  • 数组赋值的字符串,可以通过下标索引修改

这里说的 char *s 和char s[]

7.3.2 内容复制比较

  • 数组是不能用b=a,以及b==a;
  • 指针是不要用b=a,以及b==a。

7.3.3 计算内存容量

  • sizeof能够计算出数组容量的字节数,且包含"\0"
  • 当数组作为函数参数进行传递时,该数组自动退化为同类型的指针,而直接被创建a[]时,他的sizeof就是他所创建的长度。

7.4 指针参数是如何传递内存的

  • 当你在函数中申请堆栈空间时,会生成临时指针_p=p,如果_p的内容发生了修改,p的内容也会随之修改,但是申请内存,导致_p的指向已经变了。而参数p的指向却没有变,故不会成功申请。

解决方法:采用指针的指针,这样我们在申请内存改变的是指针指向的指针所指向的内容发生了改变,而指针指向的指针没有改变。

  • 也可以直接通过返回值的方式进行指针的传递。

7.5 free和delete做的事情

  • 只是把指针指向的内存释放了,指针没有被干掉

7.6 动态内存会被自动释放吗

  • 函数体里面指针声明的空间不会随着函数体结束而消亡。所以说一定要配对

7.7 杜绝“野指针”

  • 野指针不是指向NULL的指针,是指向垃圾的指针。

    1. 没有初始化
    2. 释放之后,没有NULL
    3. 指针操作超出了变量的作用范围

7.8 NEW和malloc

  • new和delete是C++针对类对象而发明的,他可以在new和delete的过程中,调用析构函数和构造函数

7.9 内存耗尽怎么办

  • new会返回NULL,表示申请内存失败
  • 可用exit(1)终止整个程序
  • visual C++中可以用_set_new_hander函数为new设置用户自己定义的函数的异常处理函数

7.10 malloc、free的使用要点

  • malloc返回的是void*类型,需要进行强制类型转换
  • 不能对同一个变量,释放两次,会报错
int *p1 = (int*) malloc(sizeof(int) * length);

int *p2 = new int[length]

7.11 new/delete的使用要点

delete []objects //正确的用法

心得体会

多使用指针, 越怕越要使用,可以大大的降低你的代码量

8 C++函数的高级特性

8.1 函数重载

8.1.1 如何区分重载

  • 参数不同才表示函数重载(因为执行函数时,可以不带返回值,所以返回值会产生二义性)

  • extern “C”声明,编译是,C的编译器会将函数名改为_foo,但是C++会将函数的变量名改为_foo_int_int,所以需要进行声明。

extern "C"
{
    void foo(int x, int y);
}

extern "C"
{
    #include "myheader.h"
}

8.1.2 隐式类型转换

因为C++面对数字变量时,如果传入0.5到int类型的函数中,会自动转换为整形变量,这样就导致重装载函数出现了二义性。

这里的二义性,是指输入的参数为常量,而不是变量,变量已经被定义过类型了,所以不会产生二义性

8.2 成员函数的重装载和覆盖

重载特征

  • 相同的范围(同一个类中)
  • 函数名字相同
  • 参数不同
  • virtual关键字可有可无

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

  • 不同的范围
  • 函数名字相同
  • 参数相同
  • 基类函数必须有virtual

如果某个基类被许多个派生类继承了,想要调用某个派生类的虚函数,采用基类指针指向子类,然后运行虚函数,方便区分。

基类指针指向子类,运行同名函数时,可以各自区分,如果是虚函数,则为子类的函数

8.3 参数的缺省值

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

8.4 运算符重载

运算符 规则
所有一元运算符 建议重载为成员函数
= () [] -> 只能重载为成员函数
+= -= /= *= &= |= ~= %= >>= <<= 建议重载为成员函数
所有其他运算符 建议重载为全局函数

8.5 函数内联

  • 用内联取代宏代码,可以提高函数的执行效率,本身宏代码使用是预处理器复制宏代码的方式代替函数调用,省去参数压栈、生成汇编语言的CALL调用,返回参数,执行return等过程,但是使用宏代码容易出现边际效应,他只是无脑的复制而已,没有安全检查。

  • 内联函数比宏代码多了安全检查,建立使用内联,但是内联是以代码膨胀(复制)为代价的,仅仅省去函数调用的开销,如果执行函数体内的代码时间较短,可以采用内联函数。

9.类的构造函数、析构函数与赋值函数

9.1 构造函数的初始化表

  • 如何类存在继承关系,派生类必须在其初始化表里调用基类的构造函数。
  • 类的const常量只能在初始化表里被初始化,因为他不能在函数体内用赋值的方式
  • 类的数据成员的初始化可以采用初始化表或函数体赋值两种方式

非内部数据类型的成员应当采用第一种方式初始化

  1. 如果类B的构造函数用初始化表的方式将成员m_a初始化的效率要高
  2. 如果上述方法采用无参构造,那么需要两步,第一步创建m_a对象,第二步,进行赋值语句

9.2 构造和析构的次序

  • 构造从类层次的最根处开始,在每一层中,首先调用基类的构造函数,然后调用成员对象的构造函数。析构严格按照相反的次序执行。

  • 成员初始化的顺序不受他们在初始化表中次序的影响,只有成员对象在类中声明的次序决定。是因为不同的构造函数可能有不同的初始化表,导致无法得到唯一的逆序。

9.3 拷贝构造和赋值函数

  • 拷贝构造最好使用()来写,用于与赋值函数区分。因为定义+赋值,会自动调用拷贝构造函数

9.4 防止内容自赋值

if(this == &other);

判断是否进行自赋值,因为有可能这种自赋值是用过间接的形式产生的。

9.5 不想处理拷贝构造函数与赋值函数

如果我们实在不想编写构造函数和赋值函数,又不允许别人使用编译器生成缺省函数,可以将其设置为私有函数,不用编写代码。

A &operator = (const A &a) {};

9.6 如何在派生类中实现类的基本函数

基类的构造函数、析构函数、赋值函数都不能被派生类继承。如果类之间存在继承关系,在编写上述基本函数时应注意以下事项

  • 派生类的构造函数应该在其初始化表里调用基类的构造函数
  • 基类与派生类的析构函数应该为虚
  • 注意基类的数据成员重新赋值

10 类的继承与组合

继承:如何类A是类B中的一种,并且B的所有功能和属性对A而言都有意义,那么A可以继承B

组合:若是在逻辑上A是B的“一部分”,则不允许B从A派生,而是通过组合得到B(正确的设计可能代码冗长,但是这是符合情理的)

11 其他编程经验

11.1 使用const提高函数的健壮性

  • 用于警惕
  • 替换#define来定义常量
  • 让返回值的内容不能被修改

const成员函数

任何不会修改数据成员的函数都应该声明为const。

11.2 提高程序的效率

时间效率是指运行速度,空间效率是指程序占用内存或者外存情况

全局效率是指在整个系统的角度上考虑的效率,局部效率是指站在模块或函数角度上考虑的效率

  • 考虑效率的同时,要注重满足正确性、可靠性、健壮性、可读性等质量因素的前提下,设法提高程序的效率。

优化标准:

  1. 以全局效率为主,提高局部效率为辅
  2. 在优化程序的效率时,找出限制瓶颈,不要在无关紧要之处优化
  3. 先优化数据结构和算法,在优化执行代码
  4. 权衡时间效率和空间效率
  5. 不必追求紧凑的代码,这不代表能产生高效的机器码

断言使用的时机

  • 可以在预计正常情况下程序不会到达的地方放置断言
  • 每个assert只检验一个条件
  • 表达式中不能改变环境,即改变变量的值
void assert(int expression);

你可能感兴趣的:(C++,c++,开发语言)