接下來的幾篇文章介紹C++的基礎知識點,
C++是一種靜態資料型別語言,它的型別檢查發生在編譯時,因此,編譯器必須知道程式中每一個變數對應的資料型別,
資料型別是程式的基礎:它告訴我們資料的意義以及我們能在資料上執行的操作,
比如:i = i + j; 這條陳述句的具體含義要取決于i、j的型別
void也是一種型別,即空型別,
算術型別:整型(integral type,包括字符和布爾型別)、浮點型
算術型別所占的位(bit)數,在不同機器上有所差別,C++標準規定了型別的最小尺寸,比如int最小占16位,char最小占8位,long最小占32位,long long最小占64位,float最小6位有效數字,double最小10位有效數字等等,
布爾型別(bool)的取值是真(true)或者假(false),
基本的字符型別是char,一個char的空間應確保可以存放機器基本字符集中任意字符對應的數字值,其他字符型別用于擴展字符集,如wchar_t、char16_t、char32_t,wchar_t型別用于確保可以存放機器最大擴展字符集中的任意一個字符,char16_t和char32_t則為Unicode字符集服務(Unicode是用于表示所有自然語言中字符的標準),
簡單介紹一下內置型別的機器實作:
計算機是以二進制形式存盤資料的,也就是每個位非0即1,一般計算機都以2的整數次冪個bit作為塊來處理記憶體,大多數以 8 bits 構成一個最小的可尋址記憶體塊,稱之為一個位元組(byte),
而計算機將記憶體中的每個位元組與一個數字(也就是地址)關聯起來,例如:

圖中,左側的數字是該位元組的地址,右側是 8 bits的具體內容,
使用某個地址來表示從這個地址開始的大小不同的位元串,例如,地址1000的那個字,地址1003的那個位元組,由此可見,為了明確某個地址的含義,必須首先知道存盤該地址的資料的型別,也就是需要知道資料所占的位數以及該如何解釋這些bit的內容,
帶符號型別(signed)和無符號型別(unsigned)
型別int、short、long和long long都是帶符號的,型別名前添加unsigned得到無符號型別,
字符型被分為了三種:char、signed char和unsigned char,特別需要注意的是:型別char和型別signed char并不一樣,型別char實際上會表現為帶符號的和無符號的兩種形式中的一種,具有是哪種由編譯器決定,
型別轉換(convert):將物件從一種給定的型別轉換為另一種相關型別,
如果運算式里既有帶符號型別又有無符號型別,當帶符號型別取值為負時會出現例外結果:
int val1 = 10;
unsigned val2 = -20;
std::cout << val1 + val2 << std::endl;
這段代碼得到的結果就不是我們想要的,因為結果會自動轉換為無符號數,
字面值常量
字面值常量的形式和值決定了它的資料型別,例如: 20 /*十進制整型字面值*/ 024 /*八進制*/ 0x14 /*十六進制*/
十進制字面值的型別是int、long和long long 中尺寸最小的那個,前提是這種型別能容納下當前的值,
short型別沒有對應的字面值,
如果我們使用了一個形如 -20 的負十進制字面值,那個負號并不在字面值之內,它的作用僅僅是對字面值取負值而已,
'a' // char型字面值(字符字面值)
"hello c++" // 字串字面值
字串字面值的型別實際上是由常量字符構成的陣列,編譯器在每個字串的結尾處添加一個空字符('\0'),因此,字串字面值的實際長度要比它的內容多1,
轉義序列:以反斜線開始,比如 \n(換行符) \r(回車符)
在程式中,轉義序列被當作一個字符使用,
變數
變數提供一個具名的、可供程式操作的存盤空間,C++中的每個變數都有其資料型別,資料型別決定著變數所占記憶體空間的大小和布局方式、該空間能存盤的值的范圍,以及變數能參與的運算,
初始化(initialized):物件被創建時獲得一個特定的值,
串列初始化:
int value = https://www.cnblogs.com/dreamerweee/p/{0};
int value2{0};
當用于內置型別的變數時,串列初始化形式有一個重要特點:如果初始值存在丟失資訊的風險,編譯器將報錯:long double ld = 3.14; int a{ld}, b = {ld}; // 錯誤,存在丟失資訊的風險
宣告和定義:宣告(declaration)使得名字可被程式知道,一個檔案如果想使用別處定義的名字則必須包含對該名字的宣告,定義(definition)負責創建與名字關聯的物體,
extern int i; // 宣告i
int j; // 宣告并定義j
extern double pi = 3.1416; // 定義,包含了顯示初始化
作用域(scope):同一個名字在不同的作用域中可能指向不同的物體,
作用域能彼此包含,被包含(或者說被嵌套)的作用域稱為內層作用域(inner scope),包含著別的作用域的作用域稱為外層作用域(outer scope),
復合型別(compound type)是指基于其他型別定義的型別,比如:參考和指標,
一條宣告陳述句由一個基本資料型別(base type)和緊隨其后的一個宣告符(declarator)串列組成,每個宣告符命名了一個變數并指定該變數為與基本資料型別有關的某種型別,
參考(reference)為物件起了另一個名字:
int ival = 1024;
int &ref_val = ival;
int &ref_val2; // 錯誤,參考必須被初始化
int &ref_val3 = 10; // 錯誤,初始值必須是一個物件
double dval = 3.14;
int &ref_val4 = dval; // 錯誤,初始值型別必須匹配
指標(pointer)是“指向(point to)”另外一種型別的復合型別,指標本身是一個物件,而參考本身不是一個物件,它只是一個別名,
空指標(null pointer)不指向任何物件,可以用字面值nullptr來指定,nullptr是一種特殊型別的字面值,它可以被轉換成任意其他的指標型別,
void*是一種特殊的指標型別,可用于存放任意物件的地址,但是,不能直接操作void*指標所值的物件,因為并不知道這個物件到底是什么型別,也就無法確定能在這個物件上做哪些操作,
// 指向指標的指標:
int ival = 1024; int *pi = &ival; int **ppi = π
// 指向指標的參考:
int *&rpi = pi;
rpi = &ival;
*rpi = 0;
const限定符
const常量的特征僅僅在執行改變該常量的操作時才會發揮作用,
當以編譯時初始化的方式定義一個const物件時,如:
const int buff_size = 512;
編譯器將在編譯程序中把用到該變數的地方都替換成對應的值,為了執行替換操作,編譯器必須知道變數的初始值,如果程式包含多個檔案,則每個用了const物件的檔案都必須得能訪問到它的初始值才行,要做到這一點,就必須在每個用到變數的檔案中都有對它的定義,為了支持這一用法,同時避免對同一變數的重復定義,默認情況下,const物件被設定為僅在檔案內有效,當多個檔案中出現了同名的const變數時,其實等同于在不同檔案中分別定義了獨立的變數,
如果要在檔案間共享,只在一個檔案中定義const,在其他對各檔案中宣告并使用它,解決方法是,對于const變數不管是宣告還是定義都添加extern關鍵字:
// file1.cpp定義并初始化
extern const int buff_size = func();
// file1.h頭檔案
extern const int buff_size; // extern的作用是指明buff_size并非本檔案所獨有
對常量的參考:
const int ci = 1024;
const int &r1 = ci;
r1 = 5; // 錯誤,r1是對常量的參考
int &r2 = ci; // 錯誤,試圖讓一個非常量參考指向一個常量物件
初始化常量參考時,允許用任意運算式作為初始值,只要該運算式的結果能轉換成參考的型別即可:
int i = 5;
const int &r1 = i;
const int &r2 = 1024;
const int &r3 = r1 * 2;
int &r4 = r1 * 2; // 錯誤,r4為非常量參考,初始值必須型別匹配
一定要注意區分指向常量的指標(pointer to const)和常量指標(const pointer),
前者表示指標所指向的物件是常量,后者表示指標本身是常量,
const double pi = 3.14;
double *ptr = π // 錯誤
const double *cptr = π // 正確,cptr就是指向常量的指標
int *const pi = &i; // 常量指標,必須初始化
這里引出:頂層const(top-level const)表示指標本身是個常量,底層const(low-level const)表示指標所指的物件是一個常量,
const int &r = ci; // 用于宣告參考的const都是底層const
常量運算式(const expression)是指值不會改變并且在編譯程序就能得到計算結果的運算式,
一個物件(或運算式)是不是常量運算式由它的資料型別和初始值共同決定,例如:
const int max_files = 30; // max_files是常量運算式
int staff_size = 27; // 不是常量運算式
const int sz = GetSize(); // sz不是常量運算式,因為它的值直到運行時才能獲取到
將變數宣告為constexpr型別,以便編譯器來驗證變數的值是否是一個常量運算式:
constexpr int mf = 30;
constexpr int sz = size(); // 只有當size是一個constexpr函式時才是一條正確的宣告陳述句
const int *p = nullptr; // p是一個指向整型常量的指標
constexpr int *q = nullptr; // q是一個指向整型的常量指標
型別別名(type alias)是一個名字,它是某種型別的同義詞:
typedef double wages; // wages是double的同義詞
using SI = SalesItem; // SI是SalesItem的同義詞(C++11)
從C++11開始,引入了auto型別說明符,用它就能讓編譯器替我們去分析運算式所屬的型別,
auto i = 0, *p = &i; // 正確,資料型別一致
auto sz = 0, pi = 3.14; // 錯誤,sz和pi的型別不一致
編譯器推斷出來的auto型別有時候和初始值的型別并不完全一樣,編譯器會適當地改變結果型別使其更符合初始化規則,
int i = 0, &r = i;
auto a = r; // a是一個整數
const int ci = i, &cr = ci;
auto b = ci; // b是一個整數
auto c = cr; // c是一個整數(cr是ci的別名,ci本身是一個頂層const)
如果希望推斷出的auto型別是一個頂層const,需要明確指出:
const auto d = ci; // d是const int
decltype型別說明符,它的作用是選擇并回傳運算元的資料型別,在此程序中,編譯器分析運算式并得到它的型別,卻不實際計算運算式的值:
decltype(f()) sum = x; // sum的型別就是函式f的回傳型別
const int ci = 0, &cj = ci;
decltype(ci) x = 0; // x的型別是const int
decltype(cj) y = x; // y的型別是const int&, y系結到變數x
如果運算式的內容是解參考操作,則decltype將得到參考型別(因為解參考指標可以得到指標所指的物件,而且還能給這個物件賦值):
int i = 42, *p = &i;
decltype(*p) c; // 錯誤,c的型別為int&,必須初始化
注意:對于decltype所用的運算式來說,如果變數名加上了一對括號,則得到的型別與不加括號時會有不同,如果decltype使用的是一個不加括號的變數,則得到的結果就是該變數的型別;如果加上了括號,編譯器就會把它當成一個運算式,得到參考型別:
decltype(i) d;
decltype((i)) e; // 錯誤,e的型別是int&,必須初始化
在《Effective C++》中,有這樣兩條建議:盡量以const,enum,inline替換#define(條款02)、盡可能使用const(條款03),
對于第一條:
例子:#define ASPECT_RATIO 1.653 記號名稱ASPECT_RATIO也許從未被編譯器看見:在預處理階段就被替換掉了, 如果運用此常量但獲得一個編譯錯誤,那可能會帶來困惑,因為錯誤資訊也許會提到1.653而不是ASPECT_RATIO 解決辦法就是用常量替換宏: const double kAspectRatio = 1.653; 定義常量指標(指標本身是常量),常量通常定義在頭檔案內(以便被不同的原始碼含入),因此有必要將指標宣告為const, const char* const kAuthorName = "Scott Meyers"; 這里,string物件更合適: const std::string kAuthorName("Scott Meyers"); class專屬常量 為了將常量的作用域(scope)限制于class內,必須讓它成為class的一個成員(member),而為確保此常量至多只有一份物體,必須讓它成為一個static成員: class GamePlayer { private: static const int kNumTurns = 5; // 常量宣告式 int m_scores[kNumTurns]; }; 上面的kNumTurns是宣告式而非定義式,必須另外提供定義式: const int GamePlayer::kNumTurns; // kNumTurns的定義,應該放在實作檔案中,宣告時已獲得初值,因此定義時不可以再設初值 如果不想讓別人獲得一個pointer或reference指向某個整數常量,可以用enum替換const, class GamePlayer { private: enum {EM_NUM_TURNS = 5}; int m_scores[EM_NUM_TURNS]; }; #define誤用情況:實作宏,宏看起來像函式,但不會招致函式呼叫帶來的額外開銷 // 以a和b的較大值呼叫函式f #define CALL_WITH_MAX(a,b) f((a) > (b) ? (a) : (b)) 帶來的問題: int a = 5, b = 0; CALL_WITH_MAX(++a, b); // a被累加兩次 CALL_WITH_MAX(++a, b + 10); // a被累加一次 取而代之的應該寫出template inline函式 template<typename T> inline void CallWithMax(const T &a, const T &b) // 由于不知道T是什么,所以采用pass by reference-to-const { f(a > b ? a : b); }
對于第二條:
STL迭代器系以指標為根據塑造出來,所以迭代器的作用就像個T*指標,宣告迭代器為const就像宣告指標為const一樣(即宣告一個T* const指標),如果希望迭代器所指的東西不可被改動(即希望STL模擬一個const T*指標),則使用const_iterator: std::vector<int> vec; const std::vector<int>::iterator iter = vec.begin(); // iter的作用像個T* const std::vector<int>::const_iterator kIter = vec.begin(); // kIter的作用像個const T* 類中不恰當的宣告const成員函式的例子: class CTextBlock { public: char& operator[](std::size_t position) const // bitwise const宣告,但其實不適當 { return m_ptext[position]; } private: char *m_ptext; }; 多載的operator[]函式,被宣告為const成員函式,但是卻回傳一個reference指向物件內部值, operator[]實作代碼并不更改m_ptext,于是編譯器很開心地為operator[]產出目標碼,但是: const CTextBlock kctb("Hello"); // 宣告一個常量物件 char *pc = &kctb[0]; *pc = 'J'; // kctb現在的內容為"Jello" mutable(可變的)釋放掉non-static成員變數的bitwise constness約束 在const和non-const成員函式中避免重復 假設TextBlock內的operator[]不單只是回傳一個reference指向某個字符,也執行邊界檢驗(bounds checking)等: class TextBlock { public: const char& operator[](std::size_t position) const { ... // 邊界檢驗 ... // 日志記錄資料訪問(log access data) ... // 檢驗資料完整性(verify data integrity) return m_text[position]; } char& operator[](std::size_t position) { ... // 邊界檢驗 ... // 日志記錄資料訪問(log access data) ... // 檢驗資料完整性(verify data integrity) return m_text[position]; } private: std::string m_text; }; 上面的代碼中包含很多重復代碼,以及伴隨的編譯時間、維護、代碼膨脹等問題,就算將邊界檢驗...等代碼移到一個函式內,也會存在重復代碼:函式呼叫、兩次return陳述句等等, 真正應該做的是實作operator[]的機能一次并使用它兩次,也就是說,必須令其中一個呼叫另一個, 將常量性轉除,這里將回傳值的const轉除是安全的: class TextBlock { public: const char& operator[](std::size_t position) const { ... ... ... return m_text[position]; } char& operator[](std::size_t position) { return const_cast<char&>(static_cast<const TextBlock&>(*this)[position]); } }; 如果不執行static_cast轉換,則會遞回呼叫自己, 令const版本呼叫non-const版本以避免重復————不應該這樣做,記住,const成員函式承諾絕不改變其物件的邏輯狀態(logical state),non-const成員函式卻沒有這般承諾,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/69143.html
標籤:C++
下一篇:stl_map復習
