Java程序員的C++回歸路(一)
前言:工作後吃飯的語言是java,同時寫一些python和js,在學習機器學習的時候發現有必要再熟悉一下c++,同時工作也有c++的使用需求。於是開始對照c++ primer自學,希望能夠對同樣是其他語言的學習者,在學習c++的時候提供一些幫助。
第1章: 起始
First program
主流編譯器:GNU 編譯器和微軟的編譯器,運行微軟編譯器的命令是:cl
Input/Output
Using namespace used to avoid inadvertent collsions between the same names.
Std::cin返回 std::cin對象
註釋
C++中有兩種類型的註釋:單行註釋://和多行註釋:/* */
如果有程序如下:
上面這段程序會一直讀取輸入的內容直到輸入的結尾,在不同的操作系統中,標識輸入的結尾是不同的,在類unix操作系統中,使用ctrl + D來表示輸入結尾,在windows系統中,使用ctrl+Z標識。
關於編譯器
在類unix系統(含Mac)使用到的編譯器和編譯指令如下:
-
cc:在Mac上,cc作為clang的軟鏈接。
-
clang:相比於gcc,clang有著編譯速度快,編譯產出小,編譯提示友好等有點。而且使用c++編寫,基於LLVM的C/C++/Objective C/Objective C++ 編譯器。
-
gcc:GNU的c編譯器,後面發展為可以編譯很多的編程語言。
-
g++:c++的編譯器。
-
msvc:windows上使用的c/c++編譯器
關於這些區別的一篇博文:https://www.cnblogs.com/qoakzmxncb/archive/2013/04/18/3029105.html
C++中的類
使用一個庫的時候,需要包含關聯的header文件。標準庫的headers一般是不包含後綴名的,編譯器一般不關心文件的後綴,但是IDE有時會。
clog的使用,默認地,寫到clog中的數據會被buffer緩沖,一般用於報告程序運行過程中的信息。
第2章:基礎
C++的原始內建類型
C++中包含了一些基本的數學類型和void類型,作為原始內建類型。
Type | Meaning | Minimum Size |
---|---|---|
bool | boolean | NA |
char | character | 8bits |
wchar_t | wide character | 16bits |
char16_t | Unicode character | 16bits |
char32_t | Unicode character | 32bits |
short | short integer | 16bits |
int | integer | 16bits(所有操作系統?) |
long | long integer | 32bits |
long long | long integer | 64bits |
float | Single-precision floating-point | 6 significant digits |
double | Double-precision | 10 significant digits |
long double | Extended-precision | 10 significant digits |
需要註意的是,在上述表格中的數據類型所占用的內存大小根據平臺的不同而不同。
確定內存地址的數據需要數據類型和讀取內存地址的二進制數據,不同的數據類型決定了占用多少比特以及如何解析這些內存數據。
上述的int、long、long long類型都是有符號數,對應的無符號數前面加上unsigned。
數據類型的後綴中是U時,字面量是unsigned類型,類型可以使unsigned int,unsigned long或unsigned long long 類型;
如果後綴是L,字面量類型時long;
如果後綴是LL,字面量類型時long long 或者unsigned long long
如果後綴是UL或ULL,字面量類型時unsigned long或unsigned long long
前綴類型表:
Meaning | Prefix | Type |
---|---|---|
u | Unicode 16 character | Char16_t |
U | Unicode 32 character | char32_t |
L | wide character | wchar_t |
u8 | utf-8 | char |
初始化和定義
C++提供了多種初始化的方式:
int unit = 0;
int unit = {0};
int unit{0};//列表初始化
int unit(0);
編譯器不允許使用列表初始化時類型信息丟失:
long double ld = 3.1415926
Declaration & Definition
聲明:聲明一個名稱讓程序知道
定義:定義除了聲明名稱和類型,同時申請內存和提供默認值
為了得到一個變量的聲明而不是定義,我們使用extern關鍵字,而且不顯示的初始化變量。
extern int i; // 聲明變量
int j; // 定義變量
一個變量可以被聲明多次,但是只能被定義一次。
作用域
全局作用域:定義在函數體之外的變量
塊作用域:{}內的作用域
復合類型
C++中有多種復合類型,這裏記錄指針和引用。
引用為對象起了另外一個名字,通過&d來定義引用類型,其中d是變量名
int a = 1;
int &d = a; // 聲明引用,d是a的另外一個名字
int &d2; // 報錯,引用必須初始化
引用非對象,相反的,它是為對象取了一個別名
為引用賦值,實際是賦值給引用的對象;獲取引用的值,獲取的是對象的值;將引用的值作為初始值,實際上是將引用對象的值作為初始值。
因為引用本身不是對象,所以不能定義引用的引用。
無法將引用綁定到另外一個對象,因此引用必須初始化。
可以給引用賦值(等於給別名賦值),如下代碼所示:
int i = 0;
int &ri = i;
ri = 10; // legal,這裏等於是給i進行賦值
指針
指針時一個指向其他類型的復合類型,值是指向對象的地址。
指針本身是一個對象。
指針值
-
指針的值可以指向一個對象
-
可以指向剛剛讀取完的對象的值(類似於Iterator執行的對象)
-
可以是個空指針,即沒有綁定任務對象
-
invalid指針,除了上述三種指針的值都是不合法的。
指針的指針
可以使用**p獲取指針的指針所在對象的值。
void*
void*是一個特別的指針類型,可以保存任意對象的地址
在理解類似於 *&p這種類型的時候,將修飾符從右往左讀去理解。
const修飾符
const修飾的類型有普通類型的大部分操作,如將const int類型轉為bool類型。
int i = 0;
const int ci = i;
int j = ci;
上述代碼中關於const的操作:給ci賦值的時候不會考慮ci是常量類型,因為不會改變常量的值,同樣的,將ci賦值給j的時候也是如此。
默認情況下,const對象僅在文件中有效。
如果需要在不同文件間共享const變量的值,則使用extern用於聲明常量並非本文件獨有。
常量引用
常量的引用類型需要使用常量引用,如下所示:
const int ci = 1;
const int &ri = ci;
int &r2 = ri; // error:non const reference to a const object
const pointer
指針本身是const,例如:
int num = 0;
int *const curNum = # // curNum will always point to num
const double pi = 3.14;
const double *const pip = π // pip is a const pointer to a const object
Top-level const
使用top-level const來標識指針本身是一個常量;如果指針指向一個const對象,那麽我們說這個const是low-level const。
constexpr
常量表達式:當做const表達式時可以使用constexpr
類型處理
typedef
alias
格式為:using a = A;
auto
自動判斷類型
decltype
自動判斷類型,但是不計算變量的值
decltype()中的解引用操作返回的結果是引用,而不是原始類型
在decltype中添加一對以上的括號,將返回引用類型
struct
//定義struct的兩種格式
struct{
};
struct {...} a,b,*c;
struct中定義的成員變量會在對象定義的時候被默認初始化。
定義頭文件
一般只定義一次的內容放在頭文件中,如類、const、constexpr變量等。
頭文件一旦改變,相關的源文件需要重新編譯來獲取新的變量聲明。
確保頭文件多次包含仍能被正確處理的機制是預處理器(preprocessor)
預處理器看到#include會將內容替換掉#include
頭文件保護符:使用以下代碼來避免重復包含的發生:
與編譯器無視關於作用域的規則
一般的做法是基於類的名字來構建保護符的名字
字:在指定機器上進行整數運算的自然單位
字符串、vector
using
格式為:using namespace::name
頭文件不應該包含using描述,否則可能有名字沖突。
宏
C++中將一個標識符定義為一個字符串,源程序中的標識符用字符串代替。如:
string
初始化string的方式
string s(4, ‘c‘) // 初始化為"cccc",直接初始化
string s2("hello") // 直接初始化
string s3 = "hello" //拷貝初始化
其他初始化方式省略
string的操作
getline(is, s) //從is中返回一行賦值給s,返回is
其他操作省略
string::size_type類型
string.size() // 返回string.size_type,實際上是一個無符號整型數
string +
string cs1 = "hello";
string cs2 = cs1 + "," + "world";
// string cs3 = "hello" + "world" + cs1; // 不能直接使用字面量相加
不能直接使用字面量相加,因為由於歷史原因,字符串字面值與string不是相同的類型
string 字符的操作
使用for循環叠代操作string中的字符,註意如果需要修改字符,需要使用引用,如下所示:
string fs("Hello,world");
for (auto &item : fs)
item = toupper(item);
std::cout << fs << std::endl; // 輸出HELLO,WORLD
使用索引來訪問字符串中的元素,需要註意的是,索引的類型也是 string::size_type
在c++標準中並不要求檢查下標是否合法,如果一個下標超出了範圍,則可能有不可預知的後果。
vector的初始化
c++提供多種初始化方式
vector<int> v1 = {1,2,3} // 拷貝初始化
vector<int> v2 {1, 2, 3} // 列表初始化
vector<int> v3(10, 1) //初始化10個1
vector的長度: size函數返回的是vector對象中元素的個數,類型是vector<xxx>:size_type類型
試圖用下標的形式訪問一個不存在的元素將引發錯誤,不過這種錯誤不會被編譯器發現,而是在運行時產生一個不可預知的錯誤。(例如緩沖區溢出(buffer overflow))
叠代器
使用叠代器:使用begin和end函數返回叠代器,其中begin成員負責返回指向第一個元素的叠代器,end成員函數返回指向容器(或string對象)尾元素的下一位置。
叠代器的*iter操作返回的是叠代器iter的引用??是否可以將iter本身理解為引用?
string sv = "some thing";
if(sv.begin() != sv.end()){
auto it = sv.begin();
*it = toupper(*it); // 使用*操作符解引用
}
泛型編程:
在c++的for循環中,經常使用!=來替代java中的<=作為判斷是否跳出循環,這個是因為在c++的標準庫中,很多的容器類提供了!=的運算符而沒有<運算符,而很多時候又使用iterator來遍歷容器。
叠代器類型
如size_type一樣,我們一般不關心叠代器的具體類型。可以是iterator或者const_iterator。
使用新標準中提供的cbegin()和cend(),返回const類型的iterator
Dereference和member
以(*iter).empty為例,*iter的括號是必須的。如果沒有括號的話將會被解析為iter.empty member,而iter是一個叠代器沒有empty member。
於此同時,c++中提供了->運算符,這個就等於(*it).
使用iterator的不能使用iterator.add()來添加元素到容器中。
vector和string提供了額外的操作,如下表所示:
使用iterator的減法返回有符號數類型的difference_type
值得註意的是,叠代器只定義了減法運算而沒有加法運算。
Array
字符數組
由於字符串由‘\0‘結束,所以在初始化char型數組時需要比字面量空間更大。
數組的初始化必須是一個初始化列表,不能用一個數組給另一個數組賦值,如下:
int[] a = {1, 2, 3};
int[] a2 = a; // Error,不能使用數組賦值
復雜的數組聲明
int (*Parray)[10] = &arr; // Parray是一個指針,指向大小為10的數組,數組類型為int
int (&arrRef)[10] = arr; //arrayRef是一個引用,引用的對象是一個大小為10的int類型的數組
在理解復雜的數組聲明時,使用由內而外的理解方法去理解。
Eg: int *(&array)[10] = ptrs; // array是一個引用,引用的對象是10個int類型的指針(Reference to an array of ten pointers)
通過下標訪問數組
通常使用sizt_t類型來定義數組的長度。
數組和指針
一般情況下,我們使用數組,編譯器會自動將指針指向數組的第一個元素。
string nums[] = {"1", "2", "3"};
string *p = &nums[0];
string *p2 = nums; //equivalent to p2 = &nums[0]
使用auto和decltype的類型:
int ia = [1, 2, 3];
auto ia2(ia2);
auto ia2(&ia[0]); // ia2的類型是int*
C++11中引入了新的獲取起始指針和尾指針的函數:begin()和end(),包含在iterator header
兩個指針相減的結果是ptrdiff_t,類似於size_t,定義在cstddef header中。
可以使用指針來操作數組,參照如下例子:
int *p = &ia[2]; // p points to the element indexed by 2
int j = p[1]; // p[1] is equivalent to *(p+1)
int k = p[-2]; // equivalent to ia[0],可以使用減法指向之前的元素
內建的數組下標可以是負數,而vector等類型的下標必須是無符號數
c語言中的string
在c標準庫中定義了一種字符串的convention,以‘\0‘結尾,定義在<string.h>中(對應c++中的cstring)
並且c標準庫中提供的函數並不會校驗傳入的字符數組是否合法,這可能會引發一些問題。
對於指針來說,如果指針指向的不是同一個對象,那麽指針之間的比較就沒有意義。
c語言中的字符串示例:
const char ca1[] = "Hello";
const char ca2[] = "World";
if(ca1 < ca2) // undifined comparison。由於實際上是指向不同對象的兩個指針做比較
c語言字符串和c++字符串之間的互相使用:
-
可以使用c語言中的null itermiated char數組,用於初始化字符串
-
可以將c語言中的字符串用作操作數
-
c++中提供c_str()成員函數,用string初始化char[]
第4章:表達式
基礎
左值和右值
一個左值表達式的求值結果是一個對象或者一個函數。
當一個對象被用作右值時,使用的是對象的值,當一個對象用作左值時,使用的是對象的地址(在內存中的位置)
值溢出
short short_value = 32767; // max value
short_value ++;
std::cout << "short_value:" << short_value << std::endl; // 發生了溢出,不同的系統上結果可能不一樣(設置可能直接崩潰),值得記錄的是,java的現象跟c++是一致的。運行結果是-32768
c++中支持使用:not來作為非條件。
第5章: Statement
第6章: 函數
try 塊
如果拋出了異常而且沒有合適的catch處理,則最終執行terminate函數,具體的處理方法與系統相關
參數
const參數
在函數定義時,使用const和不使用const是一樣的,因為使用const,頂層的const會被忽略掉。
關於const指針、引用參數的一些例子:
int i = 42;
const int *cp = &i; //正確,但是不能用cp改變i的值
const int &r = i; //正確,但是不能用r修改i的值
const int &r2 = 42; //正確,指向常量
int *p = cp; // 錯誤,指針類型不匹配
int &r3 = r; // 錯誤,類型不匹配
int &r4 = 42; // 錯誤,不能指向常量
可變形參的函數
c++支持initializer_list形參,支持同種類型不同個數的參數
同時支持可變長參數,但是一般僅僅用於和c語言交互的接口,因為很多對象不能正常的拷貝。
不要返回局部對象的引用或指針,否則將指向不可用的地址空間或對象
返回數組指針
int *p[10] :10個指針的數組
int (*p)[10]:一個指針,指向10個整數數組
返回數組指針的方式:
-
typedef
-
聲明一個返回數組指針的函數
-
使用尾指針返回類型(auto func(int i)-> int(*)[10])
-
使用decltype
引用與之類似
調試幫助
Assert 宏
assert由preprocessor處理而不是編譯器,所以使用assert時是直接使用。
NDEBUG預處理變量
當使用
在文件開始處時,代碼中的assert將不起作用,同時也可以在命令行中使用-D NDEBUG來設置。
除此之外,還可以使用NDEBUG來編寫一些根據NDEBUG的值判斷是否執行的代碼:
如果NDEBUG沒有定義,則上面塊中的代碼將被執行。
C++預處理器提供了一些用於調試的變量:
-
__func__ 當前函數
-
__FILE__ 當前文件
-
__LINE__ 當前行
-
DATE 編譯日期
-
TIME 編譯時間
重載
函數重載的選擇:所有數學轉換優先級都是相等的,如:
void test(float f);
void test(long l);
//調用test(1)時會出現ambiguous
C++中不能將const類型的引用賦值給普通類型的引用(暫時可以助記為初始化常量引用時的限制沒有普通引用多)
函數指針
定義類型: bool (*pf)(const string&)
可以直接使用函數指針來調用函數,而不需要進行解引用:
bool (*pf)(const string&);
pf("hello");
(*pf)("hello");
給函數指針賦值時,需要返回值類型和參數類型完全一致才可以賦值,函數指針之間不存在指針的轉換
函數指針指向重載函數時,需要指定具體使用的函數,通過確定的變量
函數指針作為參數,可以直接使用函數作為參數,該函數實際上會作為一個指針來處理:
void test(bool pf(const string&));
void test(bool (*pf)(const string&));
或者,也可以使用typedef和decltype來簡化代碼:
//Func和Func2是函數類型
typedef bool Func(const string&);
typedef decltype(test) Func2;
?
//Funcp和Funcp2是函數指針類型
typedef bool(*Funcp)(const string&);
typedef decltype(test) *Funcp2;
返回指針類型
//聲明函數指針
using F = int(int*); // F是一個函數類型,而不是一個函數指針
using PF = int(*)(int*); // PF是函數指針
//定義函數指針
PF f1(int);
F *f1(int);
//上面的定義等於:
int (*f1(int))(int*);
//可以使用auto和decltype
auto f1(int) -> int(*)(int*)
string::size_type sumLength(const string&, const string&){
?
}
?
string::size_type largerLength(const string&, const string&){
?
}
decltype(largerLength) *getFunc(const string&); //傳入函數名來獲取函數指針
Java程序員的C++回歸路(一)