1. 程式人生 > >第一章 初識C++

第一章 初識C++

1.1 C++概述

不同於C語言, C++是一種面向物件的語言,在C語言的基礎上,C++擴充了一些自己特有的知識,如bool型別、過載函式、模板、STL等。
C語言:面向過程的,注重過程的實現,而C++:面向物件。
舉個小例子:我吃飯。
用C語言來實現:吃(人類 變數,食物類 變數)。
用C++語言來實現:我.吃(飯)
一切都是這麼自然,符合自然規律。此外,面向物件程式設計方法還有三大特徵:封裝、繼承和多型。封裝:舉個自動洗衣機的例項。繼承:繼承父輩的一些特徵。多型:不同物件收到同一個訊息表現不同行為。例如:兩個同學面前有西瓜和蘋果。收到訊息:吃。他們兩個的行為是不同的。或者說:下課。不同同學表現的行為是不同的。(整本書均圍繞這三大特徵展開的)

1.2 C++的程式框架與C的不同

#include<stdio.h>
int main()
{
   printf(“你們好,17計科的童鞋們,我是呂聰穎\n”);
   return 0;
}

而C++:

#include <iostream>
using namespace std;
int main()
{
	cout << "你們好,17計科的童鞋們,我是呂聰穎" << endl;
	return 0;
}

第一點:標頭檔案iostream,標準的C++標頭檔案。
舊的標準C++中,使用的是iostream.h,實際上這兩個檔案是不同的,採用iostream.h導致系統速度慢,被業界恥笑,現在C++標準明確提出不支援字尾為.h的標頭檔案。還有諸如#include,#include等等,不只是形式上的改變,其實現也有所不同。
第二點:using namespace std


(1)namespace是名稱空間的關鍵字,在c++中,name可以是符號常量、變數、函式、類、物件、cout、endl等等。名稱空間實際上是由程式設計者命名的記憶體區域,他們可以把一些自己定義的變數、函式等識別符號存放在這個空間中,與其他實體定義分割開來。名稱空間的作用就是建立一些互相分割的作用域,例如三個名字相同的同學,如果分在一個班級裡,則點名時會出現不確定性,如果分在不同的班級則點名時就沒有歧義了。
(2)自己可以定義名稱空間:

namespace A
{
  int  i=10;
}
namespace B
{
   int  i=10;
   namespace C  //名稱空間的定義可以巢狀
   {
       int i=30;
   }
}
#include <iostream>
using namespace std;
int main()
{
	cout<<A::i<<endl;   或者using namespace A;
   cout<<B::i<<endl;   
	return 0;
}

問題1:使用時需要指明是哪個名稱空間的識別符號,如A::i
問題2:使用using namespace A; 可以直接使用i
問題3:名稱空間的定義能否巢狀?
問題4:C語言的缺點:要開發一個大型的資訊系統,需要呼叫多個動態庫中的函式,假如庫1和庫2均有一個數據加密函式,二者名稱相同但實現不同,在C中可能會出現呼叫錯誤的情況。而若採用C++的名稱空間,在呼叫時指明是哪個空間的函式,則可避免出錯。
(3)C++標準庫中的所有識別符號都被定義於一個名為std的namespace中,所以std又稱作標準名稱空間,要使用其中定義的識別符號就要引入std空間。

#include <iostream>
using namespace std;
int main()
{
	cout << "你們好,16計科的童鞋們,我的名字是呂聰穎" << endl;
	return 0;
}

問題1:去掉using namespace std; 這條語句會怎樣?編譯器抱怨。怎麼解決?std::cout,std::endl。std是全域性名稱空間。
(4)cin和cout
在C++中可以使用scanf和printf,但它們是c語言的標準輸入輸出函式。在C++中輸入輸出都是以“流”的形式實現的,C++定義了iostream流類庫,包含兩個基礎類istream和ostream,分別定義了標準輸入流物件cin來處理輸入,標準輸出流物件cout來處理輸出。

1.3 C++對C語言的擴充

1.3.1 bool型別(取值true 和false)

【例1】

bool  i, j,k;
i=10;
j=4>3;
k=true;
cout<<i<<”  ”<<j<<”  ”<<k<<endl;

1.3.2 C++中的型別轉換

C有隱式轉換和強制轉換,比較簡單。但並不能完全滿足C++的使用要求,所以C++提供了自己的型別轉換操作符,不管你喜不喜歡,但得保證程式執行正確。
(1) static_cast<> 靜態型別轉換操作符,最常用的一種,編譯器編譯時會做型別檢查。

          double dpi=3.14;
          int num1=(int)dpi; //C型別轉換方式
          int num2=static_cast<int>(dpi); //C++型別轉換方式

C語言中隱式型別轉換的地方均可用static_cast<>()進行型別轉換。
問題1: char*==>int *

        char*p1=”hello”;
        int*p2=NULL;
        p2=static_cast<int*>(p1); //編譯器會報錯
    此時,需要用到reinterpret_cast<>

(2)reinterpret_cast<> 強制解釋型別轉換操作符,有點強制轉換的味道 char*==>int *

    char*p1=”hello”;
    int*p2=NULL;
    p2=reinterpret_cast<int*>(p1);

(3)const_cast<>
出現了const,什麼是const? 是一個常量限定符,其作用類似於#define,但它消除了#define的不安全性,因此C++建議用const取代#define來定義常量。
【例】#define的不安全性

#include<iostream>
using namespace std;
int main()
{
   int a=1;
   #define T1 a+a     //簡單的原樣替換
   #define T2 T1-T1
   cout<<"T2 is "<<T2<<endl;
   return 0;
}


     【例】用const 取代#define
         #include<iostream>
         using namespace std;
int main()
{
   int a=1;
   const T1=a+a;   //等價於const int T1=a+a,如果const定義的是整型常量,可以省略int,常量一旦被建立,在程式的任何地方都不能被更改。
   const T2=T1-T1;
   cout<<"T2 is "<<T2<<endl;
   return 0;
}

【深入內容】const與指標的結合
【結合1】指向常量的指標:const char* pc=“hello”; 該語句定義了一個名為pc的指標變數,它指向一個字串hello。由於使用了const,不允許改變指標所指的常量,如果pc[2]=’x’是錯誤的。pc是一個指向常量的指標變數,因此可以改變pc的指向,pc=”abcd”是正確的。
【結合2】常指標:char* const pc=”hello”;該語句定義了一個名為pc的指標變數,可以改變它所指向的資料,但不能改變pc的指向,也即不能改變pc的值。如果pc[2]=’x’是正確的,但pc=”abcd”是錯誤的。
【結合3】指向常量的常指標:const char* const pc=”hello”;不能改變pc的值也不能改變pc所指向空間的資料。
【函式引數用const說明,用於保證實參在函式內部不被改動。例如,通過函式imax求陣列a[200]中的最大值,函式原型應該是:
int imax(const int* pc);確保原陣列的資料只能在函式內讀不允許寫。(舉個例子說明)
問題:如果函式原型int imax(const int* pc);這樣定義了,還想在函式內部修改原陣列內容,怎麼辦?去掉const屬性即可。

int imax(const int* pc)
{
  int*p1=NULL;
  p1=const_cast<int*>(pc);
  p1[20]=100;
   ......
}

程式設計師必須要確保記憶體空間確實能修改,否則會帶來災難性的後果。

#include <iostream>
using namespace std;
void printbuf(const char*p)
{
	char* p1=NULL;
	p1=const_cast<char*>(p);
	p1[3]='c';
}
int main()
{
	  char* pc="hello";  //char pc[]=”hello”; 換成陣列就可以。
     printbuf(pc);
	  cout<<pc<<endl;
	return 0;
}

(4)dynamic_cast<> 動態轉換操作符,只在多型型別時合法。隨後用到再講。

1.3.3 C++中的字串——string

C語言中處理字串是通過字元陣列來實現的,C++也支援這種風格,它還提供了一種自定義資料型別——string,是C++標準模板庫STL中的一個字串類,包含在標頭檔案string中。用它來定義字串,不必擔心字串長度,越界,記憶體不足等問題,string類全權負責一切事宜。
(1)用string定義字串

 #include<iostream>
#include<string>
using namespace std;
int main()
{
	  string s1;
	  s1="My dear students,";
    string s2="let's start learning ";
	  string s3("C++,");
	  string s4(6,'h');     //四種方式,其實呼叫了string的建構函式、拷貝建構函式和帶引數的建構函式
	  cout<<s1+s2+s3+s4<<endl;
	 return 0;
}

(2)通過函式實現(初始化,遍歷,連線,查詢,替換,區間刪除,反轉等操作)

1.3.4 引用

引用是隱式的指標。什麼是引用呢?通俗來說:起別名。
(1)引用的定義
【例】

int a=10;
int &b=a; //必須用相同資料型別的變數初始化。定義時就需要初始化。
b=20;

【例子】int &c=10;//錯誤的,引用在初始化時只能繫結左值,不能繫結常量值
【例】

int a=10;
int &b=a;
int j=30;
b=j;  //會發生什麼情況?

(2)C++增加引用的重要作用:作為函式引數,比指標做形參更方便

#include <iostream>
using namespace std;
void swap(int& x, int& y)
{
	int temp = x;
	x = y;
	y = temp;
}
int main()
{
	int a, b;
	cout << "please input two nums: " << endl;
	cin >> a >> b;
	swap(a, b);
	cout << "swap: " << a << " " << b << endl;
	system("pause");
	return 0;
}

使用引用就是直接操作變數,簡單高效可讀性又好。

1.3.5 動態分配記憶體(new 與delete)

C中使用malloc函式和free函式來進行動態記憶體的分配和釋放。但對於struct、enum和class等資料型別,這兩個函式無法滿足動態物件的需求,因此C++引入了new與delete來進行記憶體申請和釋放。
例子1:

#include <iostream>
using namespace std;
int main()
{
    int i;
    cin>>i;
int *pc = new int[i]; //new 一個新的字元陣列,含i個元素,沒有提供初始值
	for (int j = 0; j < i; j++)
		pc[j] = 1 ;    //向陣列中存入元素
	for (int j = 0; j < i; j++)
		cout << pc[j] << " ";
	cout << endl;
	delete[]pc;    //釋放陣列物件
	system("pause");
	return 0;
}

例子2:

#include <iostream>
using namespace std;
int main()
{
  int *pi = new int(10); //動態分配一個存放int型資料的記憶體空間,初始值為10,將首地址賦給pi ;如果int *pi=new int;則沒有提供初始值
	cout << "*pi = " << *pi << endl;
	*pi = 20;    //通過指標改變變數的值
	cout << "*pi = " << *pi << endl;
	char *pc = new char[10]; //new 一個新的字元陣列,含10個元素,沒有提供初始值
	for (int i = 0; i < 10; i++)
		pc[i] = i + 65;    //向陣列中存入元素
	for (int i = 0; i < 10; i++)
		cout << pc[i] << " ";
	cout << endl;
	delete pi;      //釋放int物件
	delete[]pc;    //釋放陣列物件
	system("pause");
	return 0;
}

問題:new可以自動計算所要分配記憶體的型別大小,不必使用sizeof()來計算所需要的位元組數。

struct node
{
	int i;
	struct node *next;
};
//p=(struct node*)malloc(sizeof(struct node)); 用C的malloc分配記憶體的方法
#include<iostream>
using namespace std;
int main()
{
   struct node *p;
   p=new struct node; //用new分配記憶體的方法
   p->i=10;
   p->next=NULL;
   cout<<p->i<<endl;
   return 0;
}

注意:為了保持與C的相容,malloc和free在C++中同樣可以使用。

1.3.6預設引數

預設。安裝軟體時,有很多選項設定了預設,可以不管它繼續安裝,也可以更改預設選項。C++的函式也支援預設引數機制,即在定義或宣告函式時給形參一個初始值,呼叫函式時,如果不傳遞實參就使用預設引數值。
例如:

#include <iostream>
using namespace std;
void add(int x, int y = 1, int z = 2); //函式宣告中有兩個形參有預設值
int main()
{
	add(1);  //只傳遞1給形參x,而y、z使用預設形參值
	add(1, 2); //傳遞1給x,2給y,而z使用預設形參值
	add(1, 2, 3); //傳遞三個引數,不使用預設形參值
	system("pause");
	return 0;
}
void add(int x, int y, int z)
{
	cout << x + y + z << endl;
}

問題1:預設引數出現的位置?如上例,既有函式宣告又有函式定義,則預設引數只能在函式宣告中設定。如果沒有函式宣告,才可以在函式定義中設定。
問題2:void add(int x, int y = 1, int z ); 錯誤,預設引數後面不能再有普通的形參,因為預設引數定義的順序是自右向左。

1.3.7行內函數

函式的呼叫有利於程式碼重用,提高效率,但有時頻繁的函式呼叫也會增加時間與空間的開銷反而造成效率低下。因為呼叫函式實際上是將程式執行順序從函式呼叫處跳轉到函式所存放在記憶體中的某個地址,將呼叫現場保留,跳轉到那個地址將函式執行,執行完畢後再回到呼叫現場,所以頻繁的函式呼叫會帶來很大開銷。
為了解決該問題,C++提供了行內函數,編譯時將函式體嵌入到呼叫處。
【例子】

#include <iostream>
using namespace std;
inline void func() //行內函數
{
	cout << "這是一個行內函數" << endl;
}
int main()
{
	func(); //行內函數呼叫
	system("pause");
	return 0;
}

當呼叫行內函數時,編譯器就會把該函式體程式碼插入到呼叫位置,如下所示:

int main()
{
	cout << "這是一個行內函數" << endl;
	system("pause");
	return 0;
}

問題1:雖然節省了開銷,但可能會造成程式碼膨脹,因此一般將結構簡單語句少的函式定義為行內函數,不可以包含複雜的控制結構,遞迴函式是不可以定義成行內函數的。
問題2:inline只是建議編譯器將函式嵌入到呼叫處,編譯器會根據函式的長度、複雜度自行決定是否把函式作為行內函數來呼叫。由此可見,行內函數是對編譯器的一種請求。

1.3.8過載函式

舉個例子:一個班級兩個同學的姓名相同(作用域相同姓名相同),但是身高、體重、性別和外貌等會有所不同,老師點名時會根據他們的特徵來區分,如高個某某,胖某某。在程式語言裡也會出現這種情況,幾個實現不同功能的函式有著相同的函式名,在呼叫時需要根據引數的不同來確定呼叫哪個函式,這就是C++提供的函式過載機制。
所謂過載函式就是在同一個作用域內幾個函式名字相同但形參列表不同。引數列表不同有三種含義:引數個數不同,或引數型別不同或者引數個數和型別都不同。
【例】過載函式例項

#include <iostream>
using namespace std;
void add(int x, int y)
{
	cout << "int: " << x + y << endl;
}
void add(float x)
{
	cout << "float: " << 10 + x << endl;
}
double add(double x, double y)
{
	return x + y;
}
int main()
{
	add(10.2);  //一個實型引數
	add(1, 3);  //兩個整型引數
	cout << add(3.2, 2.3) << endl;  //兩個實型引數
	system("pause");
	return 0;
}

注意:過載函式不能用返回值來區分。
(1)過載和預設引數
當過載的函式中,包含具有預設引數的函式時,必須注意防止呼叫的二義性。
例如:int add(int x, int y=2)
void add(int x)
如果函式呼叫:add(4),編譯器將無法確認到底要呼叫哪個過載函式。
(2)過載與const形參
當過載的函式中,包含形參由const修飾的函式時。
【例子】

#include <iostream>
using namespace std;
void func1(const int *x) //常量指標
{
	cout << "const int*: " << *x << endl;
}
void func1(int *x) //普通指標
{
	cout << "int*: " << *x << endl;
}
void func2(const int &x) //常引用
{
	cout << "const int&: " << x << endl;
}
void func2(int &x) //普通引用
{
	cout << "int&: " << x << endl;
}
int main()
{
	const int a = 1;
	int b = 2;
	func1(&a); //常量引數
	func1(&b); //非常量引數

	func2(a);  //常量引數
	func2(b);  //非常量引數
	system("pause");
	return 0;
}

注意:(1)編譯器可以根據實參是否是常量來推斷應該呼叫哪個函式。const物件只能傳遞給const形參。
(2)非const物件可以傳遞給const形參,上面的過載函式都可以被非const物件呼叫。在過載函式中,編譯器會優先選擇非常量版本的函式。