二三、編譯器 1、One Definition Rule 1)轉化單元 我們寫好的每個源文件(.cpp,.c)將其所包含的頭文件(#include <xxx.h>)合併後,稱為一個轉化單元。 編譯器單獨的將每一個轉化單元生成為對應的對象文件(.obj),對象文件包含了轉化單元的機器碼和轉化單元的引用 ...
二三、編譯器
1、One Definition Rule
1)轉化單元
我們寫好的每個源文件(.cpp,.c)將其所包含的頭文件(#include <xxx.h>)合併後,稱為一個轉化單元。
編譯器單獨的將每一個轉化單元生成為對應的對象文件(.obj),對象文件包含了轉化單元的機器碼和轉化單元的引用信息(不在轉化單元中定義的對象)。
最後鏈接器將各個轉化單元的對象文件鏈接起來,生成我們的目標程式。
比如在對象文件A中包含了定義在其它轉化單元的引用,那麼就去其它轉化單元的對象文件中尋找這個引用的定義來建立鏈接,如果在所有的對象文件中都找不到這個定義,那麼就會生成一個鏈接錯誤。
2)未定義行為
在編寫代碼中,C++標準未做規定的行為,稱為未定義行為,未定義行為的結果是不確定的,具體不同的編譯器下會有不同的效果,比如
c=2*a++ + ++a*6;
這裡先算a++還是先算++a就是一個未定義行為,比如
int x = -25602;
x= x>>2;
x的結果在不同的編譯器下是不確定的,因為這也屬於未定義行為
3)One Definition Rule(ODR)
ODR是一系列規則,而不是一個規則,程式中定義的每個對象都應有著自己的規則;但是基本上來講任何的變數、函數、類、枚舉、模板、概念(C++20)在每個轉化單元中都只允許有一個定義;
在整個程式中,非inline的函數或變數(C++17),有且僅能有一個定義
const聲明的變數或函數只在當前的源文件中有效,可以在一個項目的不同源文件定義相同的const變數
4)名稱的鏈接屬性
程式中的變數、函數、結構等都有著自己的名字,這些名字具有不同的鏈接屬性,鏈接器就是根據這些鏈接屬性來把各個對象文件鏈接起來的。鏈接屬性分為以下三種
①內部鏈接屬性:該名稱僅僅在本轉化單元中有效,如static、const聲明的變數、函數
②外部鏈接屬性:該名稱在其它轉化單元中也有效。通過extern關鍵字可以定義外部鏈接屬性
③無鏈接屬性:該名稱僅僅能夠用於該名稱的作用域內訪問
註:static變數或函數在自己的轉化單元有著自己的記憶體空間,而inline定義的變數只有一個記憶體地址
2、#define
1)用法一
#define A B //將標識符A定義為B的別名
#define 整數 int //將整數替換為int
整數 a{};
//#define實際用法
#include <iostream>
#define _HHHH_ int a //將_HHHH_ 替換為int a
#define VERSION "V2.0"
int main()
{
_HHHH_ { 250 };
std::cout << a << std::endl;
std::cout << VERSION<<std::endl;
}
2)C++中定義常量的方式
//C++定義常量的方法
const int width{1080};
//C語言中經常通常#define來定義常量
#define width 1080
#define的方式來定義常量存在一個問題,有時候並不安全
3)#define其它寫法
#define H //定義一個標識符H ,代碼中的H將會被刪除掉
int H a 相當於 int a;
//實際場景應用
#define _in_ //沒有實際意義
#define _out_
int ave(_in_ int a,_out_ int& b)
{
return a+b
}
4)取消巨集的定義
//語法
#undef H //
//應用場景
#define _H_
#undef _H_ //刪除巨集_H_的定義,後面的代碼不能使用
註:執行順序為代碼編譯的順序(從上到下),而不是函數調用的順序
5)定義複雜表達式巨集
#define SUM(X,Y) X+Y //使用X+Y替換SUM(X,Y)
#define AVE(X,Y) (X+Y)/2 //使用(X+Y)/2替換AVE(X,Y)
#define BIGGER(X,Y) ((X)>(Y)?(X):(Y))
SUM(100,200);
AVE(100,200);
BIGGER(100,200);
//實際應用場景
#define RELEASE(x) delete[] x;x=nullptr
int main()
{
int* a = new int[50];
RELEASE(a);
}
6)定義複雜表達式巨集
// #可以將一個標識符參數字元串化
#define SHOW(X) std::cout<<#X //通過#將X處理成了字元串
SHOW(1234fg); //相當於std::cout<<"12345fg"
// ##可以連接兩個標識符
#define T1(X,Y) void X##Y(){std::cout<<#Y;}
T1(test, 22);
3、namespace
有時候為了方便管理,把相關的函數、變數、結構體等會附加到一個命名空間中
//聲明命名空間
namespace t
{
int value;
}
//訪問這個命名空間的變數
t::value
//直接使用命名空間,不推薦
using namespace t;
vlaue=255;
2)全局命名空間
雖有具有鏈接屬性的對象,只要沒有定義命名空間,就預設定義在全局命令空間中,全局命名空間中成員的訪問不用顯示的指定,當局部名稱覆蓋了全局名稱時才需要顯示的指定全局命令空間
int a;
::a=250;
3)命名空間的擴展
//第二個htd屬於對htd命名空間的擴展,weight和heigth同屬於一個命名空間
namespace htd
{
int weigth{1980};
}
namespace htd
{
int heigth{1080};
}
4)命名空間的聲明
//htd.h
namespace std
{
extern int height; //變數聲明
void test(); //函數聲明
}
//htd.cpp
#include <iostream>
#include "htd.h"
int htd::heigth{250}; //變數定義
void htd::test() //函數定義
{
std::cout<<htd::height;
}
5)命名空間的嵌套
//htd.h
namespace htd
{
namespace hack //命名空間的嵌套
{
void hackServer();
}
}
//htd.cpp
void htd::hack::hackServer()
{
...
}
void htd::sendSms()
{
...
}
6)未命名的命名空間
不給命名空間指定名稱,將會聲明一個未命名的命名空間。未命名的命名空間中聲明的內容一律為內部鏈接屬性,包括extern聲明的內容,未命名的命名空間僅僅在本轉化單元中有效
//t.cpp
void THack()
{
}
//x.cpp
namespace
{
void THack()
{
}
}
int main()
{
THack();
}
//
7)命名空間的別名
namespace htd
{
void sendSms();
namespace hack
{
void hackServer();
}
}
namespace hServer=htd::hack;
hServer::hackServer();
4、預處理指令邏輯
所有#開頭的代碼都是和編譯器進行打交道
1)#ifdef
#define _HEIGHT_ 1080 //#ifdef和#endif成對出現
#ifdef _HIGHT_
#else
#endif
//hc.h
#ifdef _HC_ //如果定義了巨集_HC_,就執行XXXX裡面的代碼。如果沒有定義,則執行YYYYY
XXXX
#else
YYYYY
#endif
//常見用法
#ifndef _HC_ //如果沒有定義了巨集_HC_
#define _HC_ //則定義了巨集_HC_
#else
#endif
//實際應用場景
#ifdef UNICODE //如果使用了UNICODE字元集,則使用wchar_t定義變數
wchar_t a;
#else
char a;
#endif
//通過預處理指令進行版本控制
#define VERSION 101
#if VERSION==100 //當VERSION為100時,執行如下邏輯,否則執行else中的邏輯
void SendSms()
{
}
#else
void SendSms()
{
}
#endif
2)#elif
//#elif語法
#define _HEIGHT_ 2080
#if _HEIGHT_ == 1080 //針對每一個解析度執行不同的邏輯
...
#elif _HEIGHT_ == 720
...
#else
...
#endif
//預處理指令可以進行簡單的計算//當VERSION為100時,執行如下邏輯,否則執行else中的邏輯
void SendSms()
{
}
5、預定義巨集
1)標準預定義標識符_fun_
編譯器支持ISO C99和ISO C++ 11,即可使用該預定義標識符。用於返回函數的名稱
__func__ //返回函數的名稱
//用法示例
#include <iostream>
int main()
{
std::cout << __func__ << std::endl; //返回函數名稱,輸出main
}
2)標準預定義巨集
編譯器支持ISO C99和ISO C++17標準,即可使用以下預定義巨集
巨集 | 說明 |
---|---|
_DATE_ | 返回源文件的編譯日期 |
_TIME_ | 返回當前轉化單元的轉化時間(可理解為代碼修改的時間) |
_FILE_ | 返回源文件的名稱 |
_LINE_ | 返回當前的行 |
__cplusplus | 噹噹前單元為C++時(即.cpp文件時),__cplusplus定義為一個整數,否則不是c++文件 |
#include <iostream>
int main()
{
std::cout << __func__ << std::endl; //返回函數名稱,輸出main
std::cout << __DATE__ << std::endl;
std::cout << __TIME__ << std::endl;
std::cout << __FILE__ << std::endl;
std::cout << __LINE__ << std::endl;
std::cout << __cplusplus << std::endl;
}
3)MSVC的預定義巨集(可理解為微軟的VC編譯器中,預定義的一些巨集)
巨集 | 說明 |
---|---|
_CHAR_UNSIGNED | 如果char類型為無符號,該巨集定義為1,否則為未定義 |
_COUNTER_ | 用於計數,從0開始,每使用一次都會遞增1 |
_DEBUG | 如果設置了_DEBUG巨集,代表當前為調試狀態/lDd /mDd /mTd該巨集定義為1,否則為未定義 |
_FUNCTION_ | 返回函數名稱 ,但是不包含修飾名 |
_FUNCDNAME_ | 函數名稱 包含修飾名 |
_FUNCSIG_ | 包含了函數簽名的函數名 |
_WIN32 | 當編譯為32位ARM,64位ARM,X68或X64定義為1,否則未定義 |
_WIN64 | 當編譯為64位ARM或x64定義為1,否則未定義。用於區別 |
_TIMESTAMP_ | 最後一次源代碼修改的時間和日期 |
#include <iostream>
int main()
{
#ifdef _CHAR_UNSIGNED //如果char為無符號類型,可以通過該預定義巨集進行檢驗
std::cout << "無符號類型";
#endif
std::cout << "有符號類型" << std::endl;
std::cout << __COUNTER__ << std::endl; //代表計數
std::cout << __COUNTER__ << std::endl;
std::cout << __COUNTER__ << std::endl;
#ifdef _DEBUG //代表調試狀態
std::cout << "調試狀態" << std::endl;
std::cout << __FUNCTION__ << std::endl; //返回函數名,不包含修飾名
std::cout << __FUNCDNAME__ << std::endl; //返回函數名,包含修飾名
std::cout << __FUNCSIG__ << std::endl; //包含函數簽名,即調用、約定等信息
std::cout << __TIMESTAMP__ << std::endl; //最後一次源代碼修改的時間和日期
#endif
#ifdef _WIN32 //用於區分Win32架構或者Win64架構
std::cout << "X86" << std::endl;
#endif // _WIN32
}
只要【項目屬性】-【C/C++】-【代碼生成】-【運行庫】,選擇了調試,則_DEBUG就會顯示
註:上述巨集只在微軟的VS編輯器才可以使用,其它的編譯器無法使用
6、調試
我們編寫好程式以後,可能存在一些bug和錯誤,對於語法上錯誤,編譯器能夠直接給出提示,而對於邏輯上的錯誤,編譯器不能夠直接發現,調試就是一個找錯誤和改錯誤的過程
1)調試建議:為了方便調試,在編程風格上提出如下建議
①功能能模塊化就模塊化
②使用能夠體現出具體意義的函數名和變數名
③使用正常的縮進和代碼塊
④良好的註釋習慣
2)利用集成調試器調調試程式
VS2019繼承了一個調試器,可以利用斷點、流程跟蹤等方式來調試自己的程式
斷點就是當程式執行到斷點位置,程式就會停下來
3)利用其它調試器
OllyDbg
X96Dbg
WinDbg
4)利用預處理指令來輸出調試信息
#define _dbg_i //先定義一個巨集
#ifdef _dbg_i //如果巨集存在,則執行下麵的代碼塊
std::cout<<"調試信息";
#endif
7、assert
assert巨集需要頭文件cassert
1)assert語法
//assert語法
assert(bool表達式); //如果括弧內的bool表達式為false,則會調用std::abort()函數,彈出下麵的對話框,
2)關閉assert
//關閉assert
#define NDEBUG //可以在當前轉化單元關閉assert,但是這個定義必須放在#include <cassert>之前
3)static_assert(靜態斷言)
//static_assert用於編譯時檢查條件
static_assert(bool表達式,"Error information"); //先檢查表達式,若表達式為假,則輸出後面的錯誤信息。如果表達式為0,則程式是不進行編譯的,此處二點bool表達式只能用於常量
//C++17新語法
static_assert(bool表達式);