【C++】 C++ 基础进阶【一】易错点

简介: 本文章主要分享 C++ 的一些基础和易错点,通过比较好的编程方式和借助编译器,将节省的精力和时间用在重要的事情上。

I - 概述

本文章主要分享 C++ 的一些基础和易错点,人一天的精力和时间有限,需要节省找 bug 改 bug 和避免一些不必要的时间浪费,通过比较好的编程方式和借助编译器,将节省的精力和时间用在重要的事情上。

好钢要用在刀刃上 —— 鲁迅
:)

II - C++ 基础

2.1 - 计算机语言分类

首先可以思考一个问题
Python 与 C++ 有什么区别?

答案可以是:

  • python 语句结束不用加分号,C++ 语句结束需要加分号
  • python 中 if else 悬挂问题,else 关联最近的相同缩进的 if ,
    而 C++ else 永远关联最近的 if
    见下面 python 和 C++ 代码:

python 示例

if test >= 0
    print("test is greater than or equal to zero")
    if test == 0
        print("test is equal to zero")
else 
    print("test is less than zero")

此代码中, else 关联 test >= 0if 分支。

C/C++ 示例

if (test >= 0)
    printf("test is greater than or equal to zero");
    if (0 == test)
        printf("test is equal to zero");
else 
    printf("test differs from zero");

此代码中 else 关联 0 == testif 分支,C/C++中 else 为就近原则。

  • ... 等

但这些答案都不触及到实质。实质问题是 Python 与 C++ 属于两种不同类型的编程语言。

计算机语言分为三大类

2.1.1 - 编程语言

简而言之,用于写程序的语言,比如手机 app 应用,网站,操作系统等,比如 Python, Java, JavaScript, C#, PHP, C/C++, R, Objective-C, Swift, Go 等。

2.1.2 - 描述语言

根据规定,制约来描述和结构化数据集合的语言,例如 XML, HTML 和 JSON 等。

2.1.3 - 查询语言

用于查询存储数据的结构,常见例如,关系型数据库的查询语言 SQL, RDF 图表的 SPARQL, XML 文档的 XQuery 等。

2.2 - 编程语言分类

其中,编程语言又可以细分为三大类:解释型语言,编译型语言,伪编译型语言。

2.2.1 - 解释型语言

这类语言的源代码需要被翻译为汇编,然后一行一行地被一个程序执行,这个程序被称为解释器。例如,Python 和 PHP 就是两种解释型语言。

2.2.2 - 编译语言

此类语言所写出的源代码会被直接转变为可执行文件,在 Windows 下,它们的扩展名为 .exe 。C 和 C++ 就属于编译型语言。

2.2.3 - 伪编译语言

伪编译型语言需要借助一个伪编译器 (pseudo-compiler) 来生成一些可以在任何平台下都支持的中间文件。例如,借助 JVM 的 Java 和可以在 Microsoft .NET 平台下可用的 VB.NET,C# 等。

在执行效率方面,因为编译型语言是直接由系统执行,伪编译型语言借助平台框架或虚拟机来执行,解释型语言借助解释器翻译后来执行。所以效率上,编译语言 > 伪编译型语言 > 解释型语言。

有个朋友之前用 C++ 和 Python 写过两个相同的程序,即打开服务器上的目录并列出其包含的所有文件,如果包含子目录则递归列出其包含的所有文件。其中 C++ 程序跑了十分钟,而 Python 程序跑了几天,由此可见 Python 和 C++ 执行效率的大致情况。

2.3 - 编译过程

编译过程一共包含四个阶段:

  • 1 - 预处理
    将所有的 #include 和 #define 展开,移除注释。(.i)
    所以,不必担心注释会影响最终生成的程序体积,可以放心写注释。
  • 2 - 编译
    将经过预处理的源代码转换成汇编代码。(.s)
  • 3 - 汇编
    将汇编代码进一步转换成二进制格式的机器码,生成目标文件 (.o)
  • 4 - 链接
    将多个目标文件以及需要的库文件链接成最终的可执行文件。

所以我们在代码中的空白行也会被编译器忽略,可以大胆换行,提高代码的可读性。

2.3.1 - 前置声明

避免不必要的依赖导致编译时间过长,顶层头文件改变,所有包含此头文件的文件都需要重新生成,造成编译时间浪费,可以使用前置声明。比如:

// in header Remote.h
class DataStruct;

class Remote
{
   
//...
};

// in source file Remote.cpp
#include "../common/DataStruct.h"

2.3.2 - 默认生成

在编译过程中,C++ 的编译器会默认生成一些代码:

  1. main 函数的 return 0; 在此语句缺失时,编译器会自动补充。
  2. 类的默认构造函数和析构函数,同样在未声明和实现时。
  3. 默认的拷贝构造函数和赋值操作符重载。

第 3 点有可能造成浅拷贝 (shallow copy),和双重释放 (double free) 的崩溃问题 。

浅拷贝

  • 拷贝发生时,指针类型的类成员变量只拷贝了另一个类此成员变量的指针地址,而未重新分配一片新内存,会使得两个指针指向同一片内存,从而操作同一片内存
  • 所以对象析构的时候会释放两次同一片内存,就会出现运行时 double free 的问题

解决方法

  1. 自己实现这两个函数,拷贝构造和赋值操作符重载
  2. 设置为私有,写一个空的或者使用默认的。
    class Example
    {
         
    public:
     Example();
     ~Example();
    private:
     // make an empty one
     Example(const Example & ex) {
         }
     Example & operator=(const Example & ex) {
         }
     // or use compiler default 
     Example(const Example & ex) = default;
     Example & operator=(const Example & ex) = default;
    };
    
  3. 禁用这两个函数
    class Example
    {
         
    public:
     Example();
     ~Example();
     // disable copy constructor and assignment operator
     Example(const Example & ex) = delete;
     Example & operator = (const Example & ex) = delete;
    };
    

III - 易错点

3.1 - 宏定义

3.1.1 - 存在问题

宏定义存在的问题和替代方法。宏的缺点:

  • 缺乏类型检查,没有函数调用检查严格
  • 调试困难 (不能打断点),难定位到问题的具体位置
  • 宏只是简单的文本替换,宏展开可能产生意想不到的副作用

如:

#define SQUARE(a) (a)*(a)
int main(int argc, char * argv[])
{
   
    int i(10), r(0);
    r = SQUARE(++i); // ++i 执行了两次
}

再如优先级问题:

#define N 10
#define M 100 + 25

此时计算 MN 的结果是什么?
100 + 25
10 = 350
而不是 1250

另外,多重定义可能出现预期错误,导致下标越界,也不容易查找出错位置如:

#define PATH_MAX_SIZE 256
#define PATH_MAX_SIZE 1024

由于头文件的包含先后顺序或者包含层级等,出现不正确的宏定义。

思考? 出现两个宏定义时,代码执行时是使用的哪个宏定义?

答:后定义的宏会覆盖掉先定义的宏。

3.1.2 - 解决方法

定义常量

多数编程规范建议使用 const 表达式来替代宏,为了强制编译器类型检查,但可能还不够。

举例:

const int remoteClient = 0;
const int remoteServer = 1;
const int statusOn = 2;
const int statusOff = 1;
const int statusError = 0;

bool SetRemoteConfig(Remote * rmt, int type, int status);

SetRemoteConfig(rmt1, statusOn, remoteClient);// wrong parameters

定义相同的变量类型,可能会由于开发人员疏忽设置错误。

定义枚举

enum RemoteType {
    remoteClient, remoteServer };
enum Status {
    statusOn, statusOff, statusError };

bool SetRemoteConfig(Remote * rmt, RemoteType type, Status status);
SetRemoteConfig(rmt1, statusOn, remoteClient); // wrong parameters but compilation error
// 编译报错,statusOn 和 remoteClient 位置错误

3.1.3 - 小结

注意:这里并不是说宏不好,而是说不要滥用。

宏的优点

  • 可以使代码简洁,方便修改
  • 节省一定量重复性的代码编写工作
  • 实现多环境兼容 (多操作系统/调试发布/多版本等宏开关)
  • 函数的整体替换
  • 提高性能

关于提高性能:

一般函数调用会先进入被调用函数中,然后再回到调用函数中。小函数的频繁跳转会引起性能上的损失。有一种方法可以既避免性能损失又进行类型检查。

使用内联函数替代宏函数,由于内联函数将调用表达式用内联函数体来替换,可以避免这种性能损失,也可在编译期间进行类型检查。

例:C 标准库的 \ 中的 max 函数。

#define __max(a,b) (((a) > (b)) ? (a) : (b))

template <class T>
inline T & max(T& x, T& y)
{
   
    return (x > y) ? x : y;
}

3.2 - NULL 与 nullptr

NULL 的实质,在 C++ 中为整数 0

#ifdef __cplusplus
    #define NULL 0
#else
    #define NULL ((void *) 0)
#endif

通常在函数重载时容易出现问题,例

DataStruct(DataStruct & dt); // 1
DataStruct(DataStruct * dt); // 2
DataStruct(int , std::string param = ""); // 3

DataStruct(NULL);

此处的函数调用会调用第 3 个, nullptr 可以转换成任意类型的指针和布尔值,但是不能转换为 int

3.3 - 条件判断易错

可能由于疏忽或其他不可控因素导致相等判断的双等号少打一个,变成了恒成立的赋值语句,编译时不报错。

相等判断时,将常值置于双等号左侧,由于常值不可以修改,则编译时会报错。

int type = 9;
enum Type {
    Connection };
if (Connection == type) // 1,  compilation error, when =
if (type == Connection) // 2,  no compilation error, when =

第 1 种条件判断语句少写等号符时会编译报错,而第 2 种则不会,因此建议将常值置于相等条件判断的左侧。

3.4 - 继承与多态

3.4.1 - 知识点 struct 与 class

structclass 在 C++ 中除以下三种区别外,使用方式相同。

  1. 默认成员变量的访问权限,struct 为 public ,class 为 private
  2. 默认继承方式不同,struct 为 public 继承,class 为 private 继承
    class 和 struct 可以相互继承,默认继承方式取决于派生类。
  3. 关键字 struct 不能用于定义模板
    template <class T>  // OK
    //...
    template <struct T> // error
    //...
    

3.4.2 - 多态易错

见如下代码

class Vehicle
{
   
public:
    void run() {
    std::cout << "Vehicle" << std::endl; }
};

class Tank : public Vehicle
{
   
public:
    void run() {
    std::cout << "Tank" << std::endl; }
};

int main(int argc, char * argv[])
{
   
    Vehicle * v = new Tank();
    v->run();
}

此处 v-> run() 执行的是哪个类的 run 函数?

答:调用的是 Vehicle 类中的 run 函数,由于 run 函数不为虚函数,要调用子类中的 run 函数需要在父类 run 函数前加上 virtual 关键字。

3.4.3 - 其他易错

在使用结构体时,初始化常常使用 memset 以简化操作,但是如类中或其父类中包含虚函数时需要特别注意。

class A
{
   
public:
    A()
    {
   
        //...
        memset(this, 0, sizeof(*this));
    }
};

上述代码中的 memset 在有虚函数时,会将类的虚表指针置空,从而导致空指针,以致运行时程序异常退出。

3.5 - 赋值操作符

赋值操作符重载需要特别注意要 忽略自身拷贝

有以下代码

class String
{
   
public:
    String(const char * value)
    ~String();
    String& operator=(const String & that);
private:
    char * data = nullptr;
};
//...
    String a;
    a = a;
//...
String& String::operator=(const String & that)
{
   
    if (data) delete [] data;

    data = new char [strlen(that.data) + 1];
    strcpy(data, that.data); // 自拷贝时, that.data 已经删除,变成野指针
    return *this;
}

需要进行如下修改

String& String::operator=(const String & that)
{
   
    if (this != &that)
    {
   
        if (data) delete [] data;

        data = new char [strlen(that.data) + 1];
        strcpy(data, that.data); 
    }

    return *this;
}

参考链接:https://www.cnblogs.com/sujz/archive/2011/05/12/2044365.html

参考文档:
《华为C语言编程规范.pdf》
《华为C++语言编程规范.pdf》

目录
相关文章
|
5月前
|
编译器 C++
C++进阶之路:何为命名空间、缺省参数与函数重载
C++进阶之路:何为命名空间、缺省参数与函数重载
37 3
|
5月前
|
编译器 C++
C++进阶之路:何为运算符重载、赋值运算符重载与前后置++重载(类与对象_中篇)
C++进阶之路:何为运算符重载、赋值运算符重载与前后置++重载(类与对象_中篇)
45 1
|
5月前
|
存储 编译器 C++
C++进阶之路:何为拷贝构造函数,深入理解浅拷贝与深拷贝(类与对象_中篇)
C++进阶之路:何为拷贝构造函数,深入理解浅拷贝与深拷贝(类与对象_中篇)
49 0
|
5月前
|
安全 算法 C语言
【C++进阶】深入STL之string:掌握高效字符串处理的关键
【C++进阶】深入STL之string:掌握高效字符串处理的关键
55 1
【C++进阶】深入STL之string:掌握高效字符串处理的关键
|
5月前
|
编译器 C++
C++模板进阶
C++模板进阶
24 1
|
5月前
|
存储 算法 程序员
【C++进阶】深入STL之 栈与队列:数据结构探索之旅
【C++进阶】深入STL之 栈与队列:数据结构探索之旅
54 4
|
5月前
|
算法 安全 编译器
【C++进阶】模板进阶与仿函数:C++编程中的泛型与函数式编程思想
【C++进阶】模板进阶与仿函数:C++编程中的泛型与函数式编程思想
48 1
|
5月前
|
存储 算法 程序员
【C++进阶】深入STL之vector:构建高效C++程序的基石
【C++进阶】深入STL之vector:构建高效C++程序的基石
52 1
|
5月前
|
编译器 C++
【C++进阶】深入STL之string:模拟实现走进C++字符串的世界
【C++进阶】深入STL之string:模拟实现走进C++字符串的世界
37 1
|
5月前
|
算法 编译器 C语言
C++进阶之路:深入理解编程范式,从面向过程到面向对象(类与对象_上篇)
C++进阶之路:深入理解编程范式,从面向过程到面向对象(类与对象_上篇)
64 3