1. 程式人生 > >c++ primer 第2章 變數和基本型別

c++ primer 第2章 變數和基本型別

幾個小問題:

(1)c++語言是什麼時候檢查資料型別的?(這麼提問好像不合適,但是又不知道怎麼表達,先這樣吧)

一些語言,如smalltalk和python等,是在程式執行的時候檢查資料型別的;與之相反,c++是一種靜態資料型別語言,它的型別檢查發生在編譯時。

(2)i=i+j; 的含義?(一個小坑)

這個問題其實就是考察對資料型別的理解。c++中(不僅僅是c++),資料型別是程式的基礎,它告訴我們資料的意義以及我們能夠在資料上執行的操作。在這個問題中,如果i、j都是整型資料,那麼這條語句執行的就是最簡單的加法操作,如果是字串,就是執行的字串拼接操作。

一、基本內建型別

c++的基本資料型別可以分為兩類:算術型別、空型別。

1、空型別

      空型別不對應具體的數值,僅用在一些特殊的場合當中,比如當函式不返回任何值時就使用空型別作為函式的返回值型別

2、算術型別

      算術型別分為兩類:整型和浮點型。

      整型分為三種:布林型、字元型、整型數。

  1. 布林型別的取值為true或者false
  2. 字元型被分為了三種:char、unsigned char、signed char
    1. 儘管字元型有三種,但是字元的表現形式只有兩種:帶符號的和無符號的。型別char實際上會表現為上述兩種表現形式中的一種,具體是哪兒一種由編譯器決定。
    2. 無符號型別中所有位元都用來儲存值,例如:8位元的unsigned char可以表示0-255區間內的值。c++標準並沒有規定有符號型別應該如何表示,但是約定了在表示範圍內正值和負值的量應該平衡。因此,8位元的signed char理論上應該可以表示-127到127區間內的值,大多數計算機將實際的表示範圍定為-128 至 127。
  3. 整型數也可以分為以下幾種型別:short、int、long、long long。用於表示不同尺寸的整數。其中long long型別是在c++ 11標準中新定義的。
  4. 浮點型可以表示單精度浮點型(float)、雙精度浮點型(double)、擴充套件精度浮點型(long double)

3、如何選擇資料型別的建議?

  1. 當明確知曉數值不可能為負值時,選擇使用無符號型別。
  2. 使用int來進行整數運算。如果數值超過了int的表示範圍,則選擇使用long long型別。
  3. 在算術表示式中不要使用char或者bool,只有在存放字元或者布林值時才使用它們。因為型別char在一些機器上是有符號的,在另外一些機器上又是無符號的,所以如果使用char進行運算特別容易出問題。如果需要使用一個不大的整數,請明確指定它的型別unsigned char或signed char。
  4. 執行浮點數運算選用double。因此float通常會精度不夠,而且雙精度浮點數和單精度浮點數的計算代價相差無幾。事實上,對於某些機器來說,雙精度運算甚至比單精度還快。long double提供的精度在一般情況下是沒有必要的,況且它帶來的運算時消耗也不容忽視。

二、型別轉換

物件的型別定義了物件能包含的資料以及能參與的運算,其中一種運算被大多數型別所支援,就是將物件從一種給定的型別轉換為另一種相關型別。

當在程式的某處我們使用了一種型別,而其實物件應該取另一種型別時,程式會自動進行型別轉換。下面我們來看一下在型別轉換的時候,到底會發生什麼:

bool b=42;   //b為true 當我們把非bool型別的算術值賦給bool型別後,如初始值為0,則結果為false;否則,結果均為true
int i=b;    //i為1 當我們把一個bool型別值賦給非bool型別時,若初始值為false,則結果為0,;若初始值為true,則結果為1
i=3.14;    //i的值為3 當我們把一個浮點數賦值給整數型別時,結果僅保留浮點數中小數點之前的部分
double pi=i;    //pi為3.0 當我們把一個整數值賦值給浮點型別時,小數部分記為0。如果該整數所佔的空間超過了浮點型別的容量,精度可能會有損失。
unsigned char c=-1;    //假設char佔8位元,則c的值為255

當我們賦給無符號型別一個超出它表示範圍的值時,結果是初始值對無符號型別可表示數值總數取模後的餘數。

取模運算規則:

對於a,b,運算步驟如下:

計算c=a/b  (向負無窮方向舍入)      c=-1

計算r=a-c*b       r=-1-(-1*256)=255

signed char c2=256;    //假設char佔8位元,c2的值是未定義的

當我們賦給有符號型別一個超出它表示範圍的值時,結果是未定義的。

此時,程式可能繼續工作、可能崩潰、也可能生成垃圾資料。

三、字面值常量

整型字面值常量

我們可以將整型字面值常量寫作十進位制數、八進位制數或者十六進位制數的形式。以0開頭的整數代表八進位制數,以0x或0X開頭的代表十六進位制數。例如,我們可以用下面任意一種形式來表示數值20 : 20、024、0x14.

整型字面值具體的資料型別由它的值和符號來決定。在預設情況下,十進位制字面值是帶符號數,八進位制和十六進位制字面值既可能是帶符號的也可能是不帶符號的。十進位制字面值的型別是int、long、long long中尺寸最小的那個,前提是這種型別要能容納下當前的值。八進位制和十六進位制字面值的型別是能容納其數值的int、unsigned int、long、unsigned long、long long和unsigned long long中的尺寸最小者。型別short沒有對應的字面值。

儘管整型字面值可以儲存在帶符號資料型別中,但是嚴格來說,十進位制字面值不會是負數。比如-42,負號並不在字面值中,它的作用僅僅是對字面值取負值而已。

浮點型字面值常量

浮點型字面值常量預設是double型別。

字元和字串字面值

要注意的是編譯器會在每個字串的結尾處新增一個空字元\0,因此,字面值的實際長度比它的內容要多1.

轉義序列

有兩類字元程式設計師不能直接使用:

  1. 不可列印的字元,比如退格(或者其他控制字元),它們沒有可視的圖符
  2. 在c++語言中有特殊含義的字元(單引號、雙引號、問號、反斜線)

四、變數

       變數提供一個具名的、可供程式操作的儲存空間。

       C++中每個變數都有其資料型別,資料型別決定著變數所佔記憶體空間的大小和佈局方式、該空間能儲存的值的範圍,以及變數能參與的運算。

C++中extern關鍵字的用法

       C++語言支援分離式編譯機制,該機制允許將程式分割為若干個檔案,每個檔案都可以被獨立編譯。為了將程式分為許多個檔案,則需要在檔案共享程式碼,例如一個檔案的程式碼可能需要引用在另一個檔案中定義的變數。

       為了支援分離式編譯,C++允許將宣告和定義分離開來。變數的宣告規定了變數的型別和名字,即使得一個名字為程式所知,一個檔案如果想使用別處定義的名字則必須包含對那個名字的宣告。而定義則負責建立與名字關聯的實體,定義還會申請儲存空間。

       在C/C++中,修飾符extern用在變數或者函式的宣告前,用來說明“此變數/函式是在別處定義的,要在此處引用”。

用extern修飾變數的宣告:

       舉例來說,如果檔案a.c中需要引用檔案b.c中變數int v,就可以在a.c中宣告extern int v,然後就可以引用變數v。要注意能夠被其他模組以extern修飾符引用到的變數通常是全域性變數。

       還要注意的一點就是,extern int v可以放在檔案a.c中的任何地方,比如你可以在a.c中的函式fun()定義的開頭處宣告extern int v,然後在這個函式中就可以引用變數v了,只不過這樣只能在函式fun()中引用。這說到底還是變數作用域的問題。注意extern宣告並不是只能用於檔案作用域。

用extern修飾函式的宣告:

       從本質上來講,變數和函式沒有區別。函式名是指向函式二進位制塊開頭處的指標。

       比如在檔案b.c中有一個函式fun,其函式原型是int fun(int m),如果檔案a.c需要引用這個函式,只需要在a.c中宣告extern int fun(int m),然後就可以隨意的使用fun()了。與變數相同,extern int fun(int m)也不一定要放在a.c的檔案作用域的範圍中。

使用extern宣告外部定義和使用inlude命令包含有什麼區別呢?

       如果對C/C++熟悉的朋友可能馬上會想到,如果我們要使用另一個檔案中定義的變數或者函式,只需要把這個變數或者函式定義在一個頭檔案中,然後在需要引用它的檔案中使用include命令包含進來即可。這樣當然是沒有錯的。

       那麼這兩種方式究竟有什麼區別呢?難道沒有發現extern的引用方式比包含標頭檔案的方式要簡潔的多嗎。在任何時候,任何位置,只要你向引用另一個檔案中的變數/函式,就可以用extern宣告。當然最重要的好處是:使用extern會加速程式的編譯(確切的說是預處理)的過程,節省時間,在大型C/C++程式編譯過程中,這種差異是非常明顯的。

一個extern使用的例子。

下面有一個場景:現在有兩個檔案a.c和b.c,他們都要使用到變數int common_data,如何來實現呢?(很簡單的一個例子,不要當真哦)

(1)對於一個初學者來說(我在剛學的時候最先想到的也是這個方案),建立一個頭檔案common.h,把int common_data定義在這個標頭檔案中,然後在a.c和b.c中用include命令將common.h包含進來。乍一看是非常合理的,但是如果嘗試了會發現報下列錯誤:

變數重定義錯誤。想一下為什麼會出現這個錯誤呢?include命令就相當於把標頭檔案中檔案的內容copy到include的位置,這就相當a.c和b.c都執行了一次int common_data;在C/C++中全域性變數對整個工程是可見的,所以這樣就相當於在工程內定義了兩個同名的變數,當然會報變數重定義錯誤了。

(2)用extern來實現

b.cpp中的內容:

#include <iostream>

int common_data = 666;

void print_common_data()
{
	std::cout << common_data << std::endl;
}

a.cpp中的內容:

#include <stdlib.h>

extern int common_data;	
extern void print_common_data();

int main(int argc, char **argv)
{
	print_common_data();
	system("pause");
	return 0;
}

宣告和定義的區別看起來也許微不足道,但是實際上非常重要。如果要在多個檔案中使用同一個變數,就必須將宣告和定義分離。此時,變數的定義必須出現且只能出現在一個檔案中,而其他用到該變數的檔案必須對其進行宣告,卻絕對不能重複定義。

變數命名規範

變數命名有許多約定俗成的規範,遵守這些規範有助於提高程式的可讀性

  • 識別符號要能夠體現實際含義,如上面的common_data,表示它是一個公有變數
  • 變數名一般用小寫字母
  • 使用者自定義的類名一般用大寫字母開頭,如Sales_item
  • 如果識別符號由多個單片語成,則單詞間應該有明顯的區分,如print_common_data或者printCommonDate,而不要使用printcommondata。

五、複合型別

複合型別是指基於其他型別定義的型別。C++中有好幾種複合型別,這裡先介紹其中兩種:引用和指標。

引用

引用其實就是為物件起了一個別名,通過將宣告符寫成&xxx的形式來定義引用型別,其中xxx是宣告的變數名。

如:

int value=1024;   

int &ref_value=value;      //ref_value指向value(ref_value是value的一個別名)

使用引用的時候要注意以下幾點:

  1. 一般在初始化變數時,初始值會被拷貝到新建的物件中。然而定義引用時,程式把引用和它的初始值繫結在一起(我的理解是引用指向初始值所在的那一塊儲存空間),而不是將初始值拷貝給引用。一旦初始化完成,引用將和它的初始值物件一直繫結在一起(不能將一個已存在的引用重新定義為另一個變數的引用)。因為無法令引用重新繫結到另外一個物件,因此引用必須要進行初始化。如:int &ref_value;  //錯誤
  2. 定義了一個引用之後,對其進行的所有操作都是在與之繫結的物件上進行的。
    1. 為引用賦值,實際上就是將值賦給了與引用繫結的物件。
    2. 獲取引用的值,實際上是獲取了與引用繫結的物件的值。
    3. 以引用作為初始值,實際上是以與引用繫結的物件作為初始值。
  3. 引用並不是一個物件,而是為一個已經存在的物件所起的另外一個名字。因為引用本身不是一個物件,所以不能定義引用的引用。而且,引用只能繫結在物件上,而不能與字面值常量或者某個表示式的計算結果繫結在一起。
    1. int &ref_value=10;   //錯誤,引用型別的初始值必須是一個物件
  4. 引用的型別要與繫結的物件嚴格匹配 。

double value=3.14;

int &ref_value=value;   //錯誤

下面看一個關於引用的例子:

#include <iostream>

using namespace std;

int main(int argc, char **argv)
{
    int a=10, b=20, c=0;
    cout<<"c = "<<c<<endl;
    int &a_1=a, &b_1=b, &c_1=c;
    c_1=a_1+b_1;
    cout<<"c = "<<c<<endl;
    return 0;
}

執行結果:

     

這是一個最簡單的關於“引用”使用的例子。

指標

指標是“指向”另外一種型別物件的複合型別,與引用很相似,它也實現了對其他物件的間接訪問。

那麼指標和引用相比有什麼區別呢?

  1. 指標本身就是一個物件,允許對指標進行賦值和拷貝,而且在指標的生命週期中它可以先後指向不同的物件。
  2. 指標無須在定義的時候賦初值。引用在定義時必須賦值,並且在之後不能試圖讓它成為其他物件的引用。

關於指標的一些操作:

  1. 指標中存放的是某個物件的地址,要獲得某個物件的地址需要使用取地址符‘&’。
    1. 除了c++11中新定義的智慧指標外,其他所有指標的型別都要和它所指向的物件型別嚴格匹配。
    2. 引用不是物件,沒有實際地址,因此不能定義指向引用的指標。
  2. 如果指標指向了一個物件,如果要想訪問該物件則需要使用解引用符‘*’。如*p(p是一個指標,指向物件ival)。對指標解引用會得到指標所指向的物件,因此如果給解引用的結果賦值,實際上也就是給指標所指的物件賦值。、
  3. 空指標不指向任何物件,試圖訪問空指標都會產生錯誤,因此在試圖使用一個指標之前應該首先檢查它是否為空。獲得空指標的方法如下(只講述新標準下的方法,同時現在的程式建議使用本方法)。
    1. int *p=nullptr;   //p為空指標
    2. 說明:利用字面值常量nullptr來初始化指標,這是c++11新標準中剛引用的一種方法,nullptr是一種特殊型別的字面值,它可以被轉換成任意其他的指標型別。

下面來編寫一個簡單的示例程式。

場景:有兩個數a=10,b=0,定義一個函式swap(),在其中實現將a、b的值交換。

用指標或者用引用都可以解決這個問題。將a、b作為swap()的引數傳入。(如果有人要用全域性變數來做當然也是可以的,但是這樣就沒意思了)。

#include <iostream>

using namespace std;

//用引用
void swap1(int &a, int &b)
{
    int temp=a;
    a=b;
    b=temp;
}
//用指標
void swap2(int *a, int *b)
{
    int temp=*a;
    *a=*b;
    *b=temp;
}

int main(int argc, char **argv)
{
    int a=10, b=0;
    cout<<"before swap: a="<<a<<" b="<<b<<endl;
    //swap1(a,b);  //用引用
    swap2(&a, &b);  //用指標
    cout<<"after swap: a="<<a<<" b="<<b<<endl;
    return 0;
}

另外還要注意的是:對於兩個型別相同的合法指標,可以用 == 或者 != 來比較它們,比較的結果是布林型別。

如果兩個指標中存放的地址值相同,則它們相等;反之不相等。

兩個指標存放的地址值相同有三種可能:

       1. 它們都為空

       2. 它們指向同一個物件

       3. 它們都指向了同一個物件的下一地址。

下面用一個示例程式來感受一下:​​​​​​​

#include <iostream>

using namespace std;

int main(int argc, char **argv)
{
	int *ptr_1 = nullptr, *ptr_2 = nullptr;
	int value1 = 666, value2=999;

	cout << "兩個指標都為空的時候:" << endl;
	cout << (ptr_1 == ptr_2) << endl;

	cout << "兩個指標指向不同物件的時候:" << endl;
	ptr_1 = &value1;
	ptr_2 = &value2;
	cout << (ptr_1 == ptr_2) << endl;

	cout << "兩個指標指向同一個物件的時候:" << endl;
	ptr_1 = &value1;
	ptr_2 = &value1;
	cout << (ptr_1 == ptr_2) << endl;

	cout << "兩個指標指向同一個物件的下一地址的時候:" << endl;
	ptr_1++;
	ptr_2++;
	cout << (ptr_1 == ptr_2) << endl;

	return 0;
}

這些程式都是寫著玩的,各位大佬不要笑話。哈哈哈...