1. 程式人生 > >Java程序員的C++回歸路(一)

Java程序員的C++回歸路(一)

.com always exp ica val c語言 ... put 操作

前言:工作後吃飯的語言是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++中有兩種類型的註釋:單行註釋://和多行註釋:/* */

如果有程序如下:

#include <iostream>
int main(){
int sum = 0, value = 0;
while(std::cin >> value){
sum += value;
}
std::cout<< "Sum is: "<< sum << std::endl;// std::endl flush buffer
}

上面這段程序會一直讀取輸入的內容直到輸入的結尾,在不同的操作系統中,標識輸入的結尾是不同的,在類unix操作系統中,使用ctrl + D來表示輸入結尾,在windows系統中,使用ctrl+Z標識。

關於編譯器

在類unix系統(含Mac)使用到的編譯器和編譯指令如下:

  1. cc:在Mac上,cc作為clang的軟鏈接。

  2. clang:相比於gcc,clang有著編譯速度快,編譯產出小,編譯提示友好等有點。而且使用c++編寫,基於LLVM的C/C++/Objective C/Objective C++ 編譯器。

  3. gcc:GNU的c編譯器,後面發展為可以編譯很多的編程語言。

  4. g++:c++的編譯器。

  5. 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類型,作為原始內建類型。

TypeMeaningMinimum 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

前綴類型表:

MeaningPrefixType
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進行賦值

指針

指針時一個指向其他類型的復合類型,值是指向對象的地址。

指針本身是一個對象。

指針值

  1. 指針的值可以指向一個對象

  2. 可以指向剛剛讀取完的對象的值(類似於Iterator執行的對象)

  3. 可以是個空指針,即沒有綁定任務對象

  4. 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 = &num; // curNum will always point to num
const double pi = 3.14;
const double *const pip = &pi; // 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

頭文件保護符:使用以下代碼來避免重復包含的發生:

#ifndef CPP_TEST_SALES_DATA_H
#define CPP_TEST_SALES_DATA_H
struct sales_data{
?
};
#endif //CPP_TEST_SALES_DATA_H

與編譯器無視關於作用域的規則

一般的做法是基於類的名字來構建保護符的名字

字:在指定機器上進行整數運算的自然單位

字符串、vector

using

格式為:using namespace::name

頭文件不應該包含using描述,否則可能有名字沖突。

C++中將一個標識符定義為一個字符串,源程序中的標識符用字符串代替。如:

#define ADD (x,y) x+y
result=ADD(2, 3); // 使用 x+y替換

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++字符串之間的互相使用:

  1. 可以使用c語言中的null itermiated char數組,用於初始化字符串

  2. 可以將c語言中的字符串用作操作數

  3. 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個整數數組

返回數組指針的方式:

  1. typedef

  2. 聲明一個返回數組指針的函數

  3. 使用尾指針返回類型(auto func(int i)-> int(*)[10])

  4. 使用decltype

引用與之類似

調試幫助

Assert 宏

assert由preprocessor處理而不是編譯器,所以使用assert時是直接使用。

NDEBUG預處理變量

當使用

#define NDEBUG

在文件開始處時,代碼中的assert將不起作用,同時也可以在命令行中使用-D NDEBUG來設置。

除此之外,還可以使用NDEBUG來編寫一些根據NDEBUG的值判斷是否執行的代碼:

#ifndef NDEBUG
#endif

如果NDEBUG沒有定義,則上面塊中的代碼將被執行。

C++預處理器提供了一些用於調試的變量:

  1. __func__ 當前函數

  2. __FILE__ 當前文件

  3. __LINE__ 當前行

  4. DATE 編譯日期

  5. 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++回歸路(一)