系统学习C++(四)

165、编译预处理

C++程序编译的过程:预处理 -> 编译(优化、汇编)-> 链接

预处理指令主要有以下三种:

  1. 包含头文件:#include
  2. 宏定义:#define(定义宏)、#undef(删除宏)。
  3. 条件编译:#ifdef、#ifndef

1)包含头文件

#include 包含头文件有两种方式:

  1. #include <文件名>:直接从编译器自带的函数库目录中寻找文件。
  2. #include "文件名":先从自定义的目录中寻找文件,如果找不到,再从编译器自带的函数库目录中寻找。

#include也包含其它的文件,如:*.h、*.cpp其它的文件

C++98标准后的头文件:

  1. C的标准库:老版本的有.h后缀;新版本没有.h的后缀,增加了字符c的前缀。例如:老版本是<stdio.h>,新版本是<cstdio>,新老版本库中的内容是一样的。在程序中,不指定std命名空间也能使用库中的内容。
  2. C++的标准库:老版本的有.h后缀;新版本没有.h的后缀。例如:老版本是<iostream.h>,新版本是<iostream>,老版本已弃用,只能用新版本。在程序中,必须指定std命名空间才能使用库中的内容。

注意:用户自定义的头文件还是用.h为后缀。

2)宏定义指令

无参数的宏:#define 宏名  宏内容

有参数的宏:#define MAX(x,y)  ((x)>(y) ? (x) : (y))    MAX(3,5)  ((3)>(5) ? (3) : (5))

编译的时候,编译器把程序中的宏名宏内容替换,是为宏展开(宏替换)。

宏可以只有宏名,没有宏内容

在C++中,内联函数可代替有参数的宏,效果更好。

C++中常用的宏:

  1. 当前源代码文件名:__FILE__
  2. 当前源代码函数名:__FUNCTION__
  3. 当前源代码行号:__LINE__
  4. 编译的日期:__DATE__
  5. 编译的时间:__TIME__
  6. 编译的时间戳:__TIMESTAMP__
  7. 当用C++编译程序时,宏__cplusplus就会被定义。

3)条件编译

最常用的两种:#ifdef、#ifndef    if #define  if not #define

#ifdef 宏名

  程序段一

#else

  程序段二

#endif

含义:如果#ifdef后面的宏名已存在,则使用程序段一,否则使用程序段二。

#ifndef 宏名

  程序段一

#else

  程序段二

#endif

含义:如果#ifndef后面的宏名不存在,则使用程序段一,否则使用序段二。

4)解决头文件中代码重复包含的问题

在C/C++中,在使用预编译指令#include的时候,为了防止头文件被重复包含,有两种方式。

第一种:用#ifndef指令。

#ifndef _GIRL_

    #define _GIRL_

    //代码内容。

#endif

第二种:把#pragma once指令放在文件的开头。

#ifndef方式受C/C++语言标准的支持,不受编译器的任何限制;而#pragma once方式有些编译器不支持。

#ifndef可以针对文件中的部分代码;而#pragma once只能针对整个文件。

#ifndef更加灵活,兼容性好;#pragma once操作简单,效率高。

166、编译和链接

一、源代码的组织

头文件(*.h):#include头文件、函数的声明、结构体的声明、类的声明、模板的声明、内联函数、#defineconst定义的常量等。

源文件(*.cpp):函数的定义、类的定义、模板具体化的定义。

主程序(main函数所在的程序):主程序负责实现框架和核心流程,把需要用到的头文件用#include包含进来。

二、编译预处理

预处理的包括以下方面:

1)处理#include头文件包含指令。

2)处理#ifdef #else #endif、#ifndef #else #endif条件编译指令。

3)处理#define宏定义。

4)为代码添加行号、文件名和函数名。

5)删除注释。

6)保留部分#pragma编译指令(编译的时候会用到)。

三、编译

将预处理生成的文件,经过词法分析、语法分析、语义分析以及优化和汇编后,编译成若干个目标文件(二进制文件)。

四、链接

将编译后的目标文件,以及它们所需要的库文件链接在一起,形成一个体整。

五、更多细节

1)分开编译的好处:每次只编译修改过的源文件,然后再链接,效率最高。

2)编译单个*.cpp文件的时候,必须要让编译器知道名称的存在,否则会出现找不到标识符的错误。(直接和间接包含头文件都可以)

3)编译单个*.cpp文件的时候,编译器只需要知道名称的存在,不会把它们的定义一起编译

4)如果函数和类的定义不存在,编译不会报错,但链接会出现无法解析的外部命令。

5)链接的时候,变量、函数和类的定义只能有一个,否则会出现重定义的错误。(如果把变量、函数和类的定义放在*.h文件中,*.h会被多次包含,链接前可能存在多个副本;如果放在*.cpp文件中,*.cpp文件不会被包含,只会被编译一次,链接前只存在一个版本)

6)把变量、函数和类的定义放在*.h中是不规范的做法,如果*.h被多个*.cpp包含,会出现重定义。

7)用#include包含*.cpp也是不规范的做法,原理同上。

8)尽可能不使用全局变量,如果一定要用,要在*.h文件中声明(需要加extern关键字),在*.cpp文件中定义。

9)全局的const常量在头文件中定义(const常量仅在单个文件内有效)。

10*.h文件重复包含的处理方法只对单个的*.cpp文件有效,不是整个项目。

11)函数模板和类模板的声明和定义可以分开书写,但它们的定义并不是真实的定义,只能放在*.h文件中;函数模板和类模板的具体化版本的代码是真实的定义,所以放在*.cpp文件中。

12)Linux下C++编译和链接的原理与VS一样。

167、命名空间

在实际开发中,较大型的项目会使用大量的全局名字,如类、函数、模板、变量等,很容易出现名字冲突的情况。

命名空间分割了全局空间,每个命名空间是一个作用域,防止名字冲突。

一、语法

创建命名空间:

namespace 命名空间的名字

{

       // 类、函数、模板、变量的声明和定义。

}

创建命名空间的别名:

namespace 别名=原名;

二、使用命名空间

在同一命名空间内的名字可以直接访问,该命名空间之外的代码则必须明确指出命名空间。

1)运算符::

语法:命名空间::名字

简单明了,且不会造成任何冲突,但使用起来比较繁琐。

2using声明

语法:using 命名空间::名字

using声明名后,就可以进行直接使用名称。

如果该声明区域有相同的名字,则会报错。

3using编译指令

语法:using namespace命名空间

using编译指令将使整个命名空间中的名字可用。如果声明区域有相同的名字,局部版本将隐藏命名空间中的名字,不过,可以使用域名解析符使用命名空间中的名称。

四、注意事项

1)命名空间是全局的,可以分布在多个文件中。

2)命名空间可以嵌套。

3)在命名空间中声明全局变量,而不是使用外部全局变量和静态变量。

4)对于using声明,首选将其作用域设置为局部而不是全局。

5)不要在头文件中使用using编译指令,如果非要使用,应将它放在所有的#include之后。

6)匿名的命名空间,从创建的位置到文件结束有效。

示例:

// demo01.cpp ///////////////////////////////////////

#include <iostream>         // 包含头文件。

#include "public1.h"

#include "public2.h"

using namespace std;        // 指定缺省的命名空间。

int main()

{

       using namespace aa;

       using namespace bb;

       using bb::ab;

       cout << "aa::ab=" << aa::ab << endl;

       aa::func1();

       aa::A1 a;

       a.show();

       cout << "bb::ab=" << bb::ab << endl;

}

 ///////////////////////////////////////////////////////////

// public2.cpp ///////////////////////////////////////

#include <iostream>         // 包含头文件。

using namespace std;        // 指定缺省的命名空间。

#include "public2.h"

namespace aa

{

       int    ab = 1;        // 全局变量。

}

namespace bb

{

       int    ab = 2;        // 全局变量。

      

       void func1() {          // 全局函数的定义。

              cout << "调用了bb::func1()函数。\n";

       }

       void A1::show() {    // 类成员函数的类外实现。

              cout << "调用了bb::A1::show()函数。\n";

       }

}

///////////////////////////////////////////////////////////

// public1.cpp ///////////////////////////////////////

#include <iostream>         // 包含头文件。

using namespace std;        // 指定缺省的命名空间。

#include "public1.h"

namespace aa

{

       void func1() {          // 全局函数的定义。

              cout << "调用了aa::func1()函数。\n";

       }

       void A1::show() {    // 类成员函数的类外实现。

              cout << "调用了aa::A1::show()函数。\n";

       }

}

///////////////////////////////////////////////////////////

// public2.h ///////////////////////////////////////

#pragma once

namespace aa

{

       extern int    ab;        // 全局变量。

}

namespace bb

{

       extern int    ab ;        // 全局变量。

       void func1();       // 全局函数的声明。

       class A1              // 类。

       {

       public:

              void show();    // 类的成员函数。

       };

}

///////////////////////////////////////////////////////////

// public1.h ///////////////////////////////////////

#pragma once

namespace aa

{

       void func1();       // 全局函数的声明。

       class A1              // 类。

       {

       public:

              void show();    // 类的成员函数。

       };

}

///////////////////////////////////////////////////////////

168、C++强制类型转换

C风格的强制类型转换很容易理解,不管什么类型都可以直接进行转换,使用格式如下:

目标类型 b = (目标类型) a;

C++也是支持C风格的强制类型转换,但是C风格的强制类型转换可能会带来一些隐患,出现一些难以察觉的问题,所以C++又推出了四种新的强制类型转换来替代C风格的强制类型转换,降低使用风险。

在C++中,新增了四个关键字static_cast、const_cast、reinterpret_cast和dynamic_cast,用于支持C++风格的强制类型转换。

C++风格的强制类型转换能更清晰的表明它们要干什么,程序员只要看一眼这样的代码,立即能知道强制转换的目的,并且,在多态场景也只能使用C++风格的强制类型转换。

一、static_cast

static_cast是最常用的C++风格的强制类型转换,主要是为了执行那些较为合理的强制类型转换,使用格式如下:

static_cast<目标类型>(表达式);

1)用于基本内置数据类型之间的转换

C风格:编译器可能会提示警告信息。

static_cast:不会提示警告信息。

#include <iostream>

using namespace std;

int main(int argc, char* argv[])

{

    char cc = 'X';

    float ff = cc;     // 隐式转换,不会告警。

    float ffc = static_cast<float>(cc);  // 显式地使用static_cast进行强制类型转换,不会告警。

    double dd = 3.38;

    long ll = dd;    // 隐式转换,会告警。

    long llc = static_cast<long>(dd);  // 显式地使用static_cast进行强制类型转换,不会告警。

}

2)用于指针之间的转换

C风格:可用于各种类型指针之间的转换。

static_cast:各种类型指针之间的不允许转换,必须借助void*类型作为中间介质。

#include <iostream>

int main(int argc, char* argv[])

{

    int type_int = 10;

    float* float_ptr1 = (float *) & type_int; // int* -> float* 隐式转换无效

    // float* float_ptr2 = static_cast<float*>(&type_int); // int* -> float* 使用static_cast转换无效

    char* char_ptr1 = (char *) & type_int; // int* -> char* 隐式转换无效

    // char* char_ptr2 = static_cast<char*>(&type_int); // int* -> char* 使用static_cast转换无效

    void* void_ptr = &type_int; // 任何指针都可以隐式转换为void*

    float* float_ptr3 = (float *)void_ptr; // void* -> float* 隐式转换无效

    float* float_ptr4 = static_cast<float*>(void_ptr); // void* -> float* 使用static_cast转换成功

    char* char_ptr3 = (char *)void_ptr; // void* -> char* 隐式转换无效

    char* char_ptr4 = static_cast<char*>(void_ptr); // void* -> char* 使用static_cast转换成功

}

3)不能转换掉expression的const或volitale属性

#include <iostream>

int main(int argc, char* argv[])

{

    int temp = 10;

    const int* a_const_ptr = &temp;

    int* b_const_ptr = static_cast<int*>(a_const_ptr); // const int* -> int* 无效

    const int a_const_ref = 10;

    int& b_const_ref = static_cast<int&>(a_const_ref); // const int& -> int& 无效

    volatile int* a_vol_ptr = &temp;

    int* b_vol_ptr = static_cast<int*>(a_vol_ptr); // volatile int* -> int* 无效

    volatile int a_vol_ref = 10;

    int& b_vol_ref = static_cast<int&>(a_vol_ref); // volatile int& -> int& 无效

}

169、C++类型转换-static_cast

C风格的类型转换很容易理解:

语法:(目标类型)表达式目标类型(表达式);

C++认为C风格的类型转换过于松散,可能会带来隐患,不够安全。

C++推出了新的类型转换来替代C风格的类型转换,采用更严格的语法检查,降低使用风险。

C++新增了四个关键字static_cast、const_cast、reinterpret_castdynamic_cast,用于支持C++风格的类型转换。

C++的类型转换只是语法上的解释,本质上与C风格的类型转换没什么不同,C语言做不到事情的C++也做不到。

语法:

static_cast<目标类型>(表达式);

const_cast<目标类型>(表达式);

reinterpret_cast<目标类型>(表达式);

dynamic_cast<目标类型>(表达式);

一、static_cast

1)用于内置数据类型之间的转换

除了语法不同,C和C++没有区别。

#include <iostream>

using namespace std;

int main(int argc, char* argv[])

{

    int    ii = 3;

    long ll = ii;                     // 绝对安全,可以隐式转换,不会出现警告。

    double dd = 1.23;

    long ll1 = dd;                  // 可以隐式转换,但是,会出现可能丢失数据的警告。

    long ll2 = (long)dd;              // C风格:显式转换,不会出现警告。

    long ll3 = static_cast<long>(dd);    // C++风格:显式转换,不会出现警告。

    cout << "ll1=" << ll1 << ",ll2=" << ll2 << ",ll3=" << ll3 << endl;

}

2)用于指针之间的转换

C风格可以把不同类型的指针进行转换。

C++不可以,需要借助void *。

#include <iostream>

using namespace std;

void func(void* ptr) {   // 其它类型指针 -> void *指针 -> 其它类型指针

    double* pp = static_cast<double*>(ptr);

}

int main(int argc, char* argv[])

{

    int ii = 10;

    //double* pd1 = &ii;                      // 错误,不能隐式转换。

    double* pd2 = (double*) &ii;      // C风格,强制转换。

    //double* pd3 = static_cast<double*>(&ii);    // 错误,static_cast不支持不同类型指针的转换。

    void* pv = &ii;                               // 任何类型的指针都可以隐式转换成void*。

    double* pd4 = static_cast<double*>(pv);  // static_cast可以把void *转换成其它类型的指针。

    func(&ii);

}

二、const_cast

static_cast不能丢掉指针(引用)的constvolitale属性,const_cast可以。

示例:

#include <iostream>

using namespace std;

void func(int *ii)

{}

int main(int argc, char* argv[])

{

       const int *aa=nullptr;

       int *bb = (int *)aa;                          // C风格,强制转换,丢掉const限定符。

       int* cc = const_cast<int*>(aa);      // C++风格,强制转换,丢掉const限定符。

       func(const_cast<int *>(aa));

}

三、reinterpret_cast

static_cast不能用于转换不同类型的指针(引用)(不考虑有继承关系的情况),reinterpret_cast可以。

reinterpret_cast的意思是重新解释,能够将一种对象类型转换为另一种,不管它们是否有关系。

语法:reinterpret_cast<目标类型>(表达式);

<目标类型>(表达式)中必须有一个是指针(引用)类型。

reinterpret_cast不能丢掉(表达式)constvolitale属性。

应用场景:

1reinterpret_cast的第一种用途是改变指针(引用)的类型。

2reinterpret_cast的第二种用途是将指针(引用)转换成整型变量。整型与指针占用的字节数必须一致,否则会出现警告,转换可能损失精度。

3reinterpret_cast的第三种用途是将一个整型变量转换成指针(引用)。

示例:

#include <iostream>

using namespace std;

void func(void* ptr) { 

    long long ii = reinterpret_cast<long long>(ptr);

    cout << "ii=" << ii << endl;

}

int main(int argc, char* argv[])

{

    long long ii = 10;

    func(reinterpret_cast<void *>(ii));

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值