C++概念
C++基本知識
C++基於過程
C++基於對象
C++面向對象
C++概念
C++是一門面向對象的編程語言,可以理解為是C語言的擴充,C語言為C++語言的子集,C++中增加了C語言中所沒有的面向對象的特征。面向對象有三大特征,分別是封裝、繼承和多態。
三大特征的概念:
封裝:盡可能地隱藏對象的內部實現細節,控制用戶對類的修改和訪問的程度以及權限。(封裝就是隱藏)
繼承:繼承可以使用不同的類的對象具有相同的行為;為了使用其他類的方法,我們沒有必要重新編寫這些舊方法,只要這個類(子類)繼承包含的方法的類(父類)即可。 從下往上看,繼承可以重用父類的功能;從上往下看,繼承可以擴展父類的功能。
多態:多態可以使我們以相同的方式處理不同類型的對象。我們可以使用一段代碼處理不同類型的對象,只要他們繼承/實現了相同的類型。這樣,我們沒有必要為每一種類型的對象撰寫相同的邏輯,極大的提高了代碼的重用。
C++面向對象的技術實現:
封裝:類、訪問修飾符(public、protected、private)、
繼承:繼承(泛化)、組合(聚合)
多態:函數重載、函數重寫、函數模板、類模板
======================================================================
C++基本知識
-
數據類型
文字常量
變量
const限定修飾符
指針
數組
字符串
引用
枚舉
聯合體
結構體
復數類型
pair類型
容器類型
typedef修飾符
volatile限定修飾符
-
表達式
復數操作
條件操作符
sizeof操作符
new和delete操作符
位操作符
bitset類型
運算符優先級
類型轉換
-
語法
聲明語句
數據類型
基本數據類型:整型(int)、字符型(char)、實型{單精度型(float)、雙精度型(double)}、布爾型(bool)、無值型(void)
非基本數據類型:數組(type [])、指針(type*)、引用(type&)、類(class)、結構體(struct)、枚舉型(enum)、聯合體(union)
注:不同的操作系統中,C++的數據類型所占用的字節數是不同的,可以通過sizeof操作符來判斷在當前操作系統中數據類型所占用的字節數。
文字常量
當一個數值,例如 1,出現在程序中時,它被稱為文字常量 (literal constant):稱之為”文字“是因為我們只能以它的值的形式指代它,稱之為“常量”是因為它的值不能被改變。每個文字都有相應的類型,例如 0是 int 型,而3.14159是double型的文字常量,是不可尋址的(nonaddressable),盡管它的值也存儲在機器內存的某個地方,但是我們沒有辦法訪問它們的地址。
整數文字常量可以被寫成十進制,八進制或者十六進制的形式:
十進制無任何標識的數字常量,如8
八進制前綴為0,如010
十六進制前綴為0x,如0x8
缺省情況下,整數文字常量會被當做是int類型的有符號值。可以在文字常量的末尾添加“L”或“l”符號表示長整型,添加“U”或“u”符號表示無符號型。如:12UL、12ul、12Lu、12L等。
缺省情況下,浮點型文字常量會被當做是double類型的值。可通過”f“、”F”后綴來表示單精度類型。同時,可通過“L”、“l”后綴來擴展精度值。(注:“f"、"F"、”L“、”l“后綴只能用在十進制中)
字符型(char)文字常量:' '、'a'、'd'
字符串文字類型為常量字符數組:“a”、"hello"
布爾型(bool)文字常量:true、false
寬字符型(wchar_t)文字常量:L'a'
常量寬字符數組:L”hello“
變量
變量的概念:變量為我們提供了一個有名字的內存存儲區,可以通過程序對其進行讀、寫和處理。C++中的每個符號變量都與一個特定的數據類型相關聯,這個類型決定了相關內存的大小、布局、能夠存儲在該內存區的值的范圍以及可以應用其上的操作集 。變量與常量的區別在於,變量是可尋址的。每個變量都與兩個值相關聯——1.它的數據值,存儲在某個內存地址中,也可被稱為右值;2.它的地址值,有時被稱為左值,我們可以認為左值是地址值。
變量名:即變量的標識符,由數字、字符和下划線組成,但標識符首字符只能以字符或下划線開頭。
const限定修飾符
const修飾變量時,可以將變量變成常量。被const修飾的變量必須在聲明的同時初始化。如果有操作試圖修改const修飾的變量,將發生編譯錯誤。
指針
指針的概念:表示地址值。如果想要操作該指針地址所指向的內存空間的內容,需要解引用操作符“*”解除指針的引用。
數組
數組的概念:單一數據類型的集合。其中單個變量並未對其命名,可以通過其在數組中的位置進行訪問,這種訪問成為索引訪問或下標訪問。
數組與指針類型的關系:數組標識符代表數組中第一個元素的地址,它的類型是數組元素類型的指針。(如ia和&ia[0]是等價的,同樣*ia和ia[0]也是等價的。)
字符串
C++提供了兩種字符串的表示:C 風格的字符串和標准C++引入的string 類類型(一般建議使用string型字符串)。string類型由STL提供,將在STL專題中詳解。
引用
引用的概念:引用(reference)有時候又稱之為別名(alias),因為它可以看作是對象的另一個名字。引用類型在定義的時候必須被初始化,因為引用類型一旦引用被定義后,就不能再指向其它對象。
引用類型的定義形式:int a=1024;int &b=a;
枚舉
枚舉類型的定義形式:
enum type_name{one,two,three};//枚舉類型定義后,type_name定義的變量類型只能為one,two,three。
type_name type_value=one;
聯合體
/*聯合體定義與使用*/
union typeName{
char c;
int i;
int ii[10];
}
typeName t;
聯合體變量的內存大小為該聯合體類型包含的元素類型中占用字節最多的元素類大小。如上面的聯合體類型中,元素c占用一個char大小,元素i占用一個int大小,元素ii占用十個int大小,所有該聯合體類型typeName定義的變量t大小為十個int大小。
結構體
/*結構體定義與使用*/
struct typeName{
char c;
int i;
int ii[10];
}
typeName t;
結構體的大小為結構體類型內包含所有元素的類型大小之和。
復數類型
復數類是C++標准庫的一部分,包含在頭文件#include<
complex>中。
復數的概念:復數有兩部分組成:實數部分和虛數部分。其表現形式如:3+4i
復數對象的定義:
complex
complex
complex
complex
pair類型
pair類型是C++標准庫的一部分,包含在頭文件#include
pair類型的概念:
pair對象的定義:
pair<string,string> p1("name1","name2");
可以用成員訪問符號“.”來訪問pair單個成員的內容,如:p1.first;p1.second;
容器類型
容器的定義:在數據存儲上,有一種對象類型,它可以持有其它對象或指向其它對像的指針,這種對象類型就叫做容器。朴素地看,容器就是存儲其它對象的對象。
C++的容器由C++標准模板庫(STL)提供,STL 對定義的通用容器分三類:順序性容器、關聯式容器和容器適配器。
順序性容器:順序性容器是一種各元素之間有順序關系的線性表,是一種線性結構的可序群集。順序性容器中的每個元素均有固定的位置,除非用刪除或插入的操作改變這個位置。這個位置和元素本身無關,而和操作的時間和地點有關,順序性容器不會根據元素的特點排序而是直接保存了元素操作時的邏輯順序。(vector、deque、list)
關聯式容器:關聯式容器和順序性容器不一樣,關聯式容器是非線性的樹結構,更准確的說是二叉樹結構。各元素之間沒有嚴格的物理上的順序關系,也就是說元素在容器中並沒有保存元素置 入容器時的邏輯順序。但是關聯式容器提供了另一種根據元素特點排序的功能,這樣迭代器就能根據元素的特點“順序地”獲取元素。(set/multiset、map/multimap)
容器適配器:簡單的理解容器適配器,其就是將不適用的序列式容器(包括 vector、deque 和 list)變得適用。(stack、queue、priority_queue)
關於STL容器的總體與詳細內容,將在《C++標准模板庫(STL)詳解》中介紹。
typedef修飾符
typedef的定義形式:typedef 數據類型 標識符;//如typedef int my_int;
typedef並未引入新類型,只是為數據類型引入了一個助記符。(與引用相似的地方在於,引用相當於為變量起別名,而typedef相當於為數據類型起別名。)
volatile限定修飾符
volatile修飾符與const的相似之處在於,它們都是附加修飾符。由於編譯器會在處理源代碼時對代碼做一些優化,而有時候對於有些值這些優化將導致出錯。volatile的主要作用在於提示編譯器,不要武斷地對被volatile修飾的值進行優化。
表達式
復數操作
條件操作符
操作形式:判斷表達式?判斷為真時執行此表達式:判斷為假時執行此表達式;
sizeof操作符
作用:返回一個對象或類型名的字節長度,其返回值為size_t類型。
三種操作方式:
- sizeof(type_name);//如sizeof(類型名)
- sizeof(object);//如sizeof(對象名)
- sizeof object;//如sizeof 對象名
new和delete操作符
系統為每個程序都提供了一個在程序執行時可用的內存池,這個可用內存池被稱為程序的空閑存儲區(free store)或堆(heap)運行時刻的內存分配被稱為動態內存分配(dynamic memory allocation)。動態內存分配由new表達式應用在一個類型指示符(specifier)上來完成。類型指示符可以是內置類型或用戶定義類型。
如:int *a=new int;delete a;或者int *a=new int[n];delete[] a;
位操作符
操作符 | 功能 | 用法 |
---|---|---|
~ | 按位非 | ~expr |
<< | 左移 | expr1 << expr2 |
>> | 右移 | expr1 >> expr2 |
& | 按位與 | expr1 & expr2 |
^ | 按位異或 | expr1 ^ expr2 |
| |
按位或 | expr1 | expr2 |
&= | 按位與賦值 | expr1 &= expr2 |
^= | 按位異或賦值 | expr1 ^= expr2 |
| = |
按位或賦值 | expr1 | expr2 |
00000101
^ 00000110
---------
00000011
bitset類型
運算符優先級
類型轉換
語法
聲明語句
======================================================================
C++基於過程
域和生命周期
C++作用域的關鍵詞:extern
C++生命周期的關鍵詞:static
C++支持三種形式的域:局部域、名字空間域、類域。
全局域:全局對象和非inline全局函數只能定義一次,inline函數可以被定義多次。
函數聲明:函數聲明(declaration)指定了該函數的名字以及函數的返回類型和參數表。
函數定義:函數定義(definition)包含了函數聲明部分,同時還為函數提供了函數體。
extern關鍵詞:修飾符extern用在變量或者函數的聲明前,用來說明“此變量/函數是在別處定義的,要在此處引用”。extern聲明不是定義,即不分配存儲空間。extern “c”表示C語言代碼。
在局部域中的變量聲明引入了局部對象。有三種局部對象:自動對象、寄存器對象(register關鍵詞)以及局部靜態對象(static關鍵詞)。
函數
概念:函數由返回值類型、函數名、參數列表和函數體構成。
函數形式:返回值類型 函數名(參數列表){函數體}
函數原型:函數原型由返回值類型、函數名和參數列表構成。
函數傳參
指針參數和引用參數的區別:指針可以指向任何對象,也可以不指向任何對象,在知道指針指向一個有效對象之前不能輕易的對指針進行解引用;引用則必須明確的指向一個對象,且一旦指向之后就不可以更改,否則就會報錯。
例如:
void f(int* a,int& b){
if(!a)std::cout<<*a<<std::endl;//當參數為指針時,必須先判斷該指針是否指向一個有效對象,再使用。
std::cout<<b<<std::endl;//當參數為引用時,可以直接使用
}
int main(){
int i=0;
f(&i,i);//正確
f(nullptr,i);//正確
f(nullptr,0);//錯誤表示!引用參數必須為一個對象,不能是常量
return 0;
}
所以,如果一個參數可能在函數中指向不同的對象,或者不指向任何對象,則必須使用參數指針。當一個參數在函數中只指向一個確定的對象,則使用引用參數。
重載函數
概念:在同一作用域內,一組函數的函數名相同,參數列表不同(參數個數不同/參數類型不同),返回值可同可不同。
函數重載:
1、具有相同的名稱,執行基本相同的操作,但是使用不同的參數列表。
2、函數的多態性。
3、編譯器通過調用時參數的個數和類型確定調用重載函數的哪個定義。
4、只有對不同的數據集完成基本相同任務的函數才應重載。
函數重載的優點:
1、減少了函數名的數量
2、增加了代碼的可讀性,有助於理解和調試代碼
3、易於維護代碼
函數重載是一種靜態多態。(關於多態的詳細全面的內容,將在《C++三大特性(封裝、繼承和多態)及其技術實現詳解》一文中詳解)
C++函數重載的原理:
函數重載解析:把函數調用與重載函數集合中的一個函數相關聯的過程 。
編譯器在編譯.cpp文件中當前使用的作用域里的同名函數時,根據函數形參的類型和順序會對函數進行重命名(不同的編譯器在編譯時對函數的重命名標准不一樣)但是總的來說,他們都把文件中的同一個函數名進行了重命名;
- 在vs編譯器中:
根據返回值類型(不起決定性作用)+形參類型和順序(起決定性作用)的規則重命名並記錄在map文件中。 - 在linux g++ 編譯器中:
根據函數名字的字符數+形參類型和順序的規則重命名記錄在符號表中;從而產生不同的函數名,當外面的函數被調用時,便是根據這個記錄的結果去尋找符合要求的函數名,進行調用;
為什么C語言沒有函數重載機制?
編譯器在編譯.c文件時,只會給函數進行簡單的重命名;具體的方法是給函數名之前加上”_”;所以加入兩個函數名相同的函數在編譯之后的函數名也照樣相同;調用者會因為不知道到底調用那個而出錯;
函數模板
關鍵字:template、class、typename
例:
/*a.h*/
template <class Type,int size>
Type functionName(Type (&type_name)[size]){}//當調用此函數時,需要在Type傳入數據類型,size位置傳入int型常量
/*b.c*/
#include "a.h"
int main(){
int ia[5]={1,2,3,4,5};
/*函數模板實例化*/
int i=functionName(ia);//該模板函數的Type為int,size為5.
return 0;
}
模板編譯模式分為:分離編譯模式、包含編譯模式
包含編譯模式:函數模板的聲明和定義都放在頭文件中。
分離編譯模式:函數模板的聲明放在頭文件中,定義放在源文件中。
顯式實例化聲明:
如:
/*函數模板*/
template <class T>
T f(T t,int i);
/*顯式實例化聲明*/
template int f(int t,int i);
模板顯式特化:
定義:
異常處理
關鍵字:try、catch、throw
概念:異常是程序可能檢測到的,運行時刻不正常的情況,如除0、數組訪問越界或存儲內存耗盡等。
泛型算法
======================================================================
C++基於對象
-
類
類定義和訪問權限
友元訪問
-
類的初始化、賦值和析構
類的構造函數
類的數據成員初始化存在問題,仍未解決!!!!
類的析構函數
-
重載操作符和用戶定義的轉換
操作符重載
友元訪問在操作符重載的應用
-
類模板
類模板的定義與實例化
類模板中的友元聲明
類模板的靜態數據成員
類
類定義和訪問權限
類聲明
類聲明的形式:class 類名;
例如:class Vector;
聲明一個Vector類類型,但是由於未定義,不知道該類類型的大小,編譯器不知道為該類類型的對象預留多少空間,所以也無法定義該類類型對象。但是,可以聲明一個指向該類類型的指針或引用,因為指針和引用的內存大小與所指向的對象大小無關。
類定義
類定義包含兩部分:類頭,由關鍵字class和類名構成;類體,花括號內內容。
類體定義了類成員表,類成員表包括數據成員和成員函數。
成員訪問
信息隱藏:防止程序的函數直接訪問類的內部信息而提供的一種機制。通過訪問限定符(public、protected、private)來實現。
公有成員(public):在程序的任何地方都可以被訪問。
私有成員(private):只能被成員函數和類的友元訪問。
被保護成員(protected):派生類可訪問,非派生類不可訪問。
友元訪問
在某些情況下,允許某個函數而不是整個程序可以訪問類的私有成員,這樣做會比較方便,友元機制允許一個類授權其他的函數訪問它的非公有成員。友元聲明以關鍵字 friend 開頭,它只能出現在類的聲明中。
友元可分為:友元類、友元函數
詳細內容見友元訪問在操作符重載的應用
類的初始化、賦值和析構
類的構造函數
關鍵詞:explicit、inline、const、volatile
構造函數概念:類的構造函數與類名同名,且無返回值類型。C++可以有多個構造函數,唯一的要求是這些函數參數列表不能相同。一般構造函數用來初始化類中的數據成員,對於有些需要傳參的構造函數,當參數經常使用默認值時,可以使用缺省參數。
例如:
/*例1:構造函數使用缺省參數的類*/
class Account{
public:
//缺省構造函數
Account(){}
Account(int i,int j=111,int x=111):i_(i),j_(j),x_(x){std::cout<<"使用缺省實參的構造函數"<<std::endl;}
private:
int i_;
int j_;
int x_;
};
/*例2:使用普通構造函數的類*/
class Account{
public:
//缺省構造參數
Account(){}
Account(int i,int j,int x):i_(i),j_(j),x_(x){std::cout<<"普通構造函數"<<std::endl;}
Account(int i,int j):i_(i),j_(j){
x_=111;
}
Account(int i):i_(i){
j_=111;
x_=111;
}
private:
int i_;
int j_;
int x_;
};
例1和例2中的類的構造函數結果是等價的,都可以傳入一個參數,兩個參數,或者三個參數。但是從效果上來看,例1中利用缺省參數的構造函數更好,節省代碼量,也利於理解。
構造函數重點:缺省構造函數、限制對象創建、拷貝構造函數
1、缺省構造函數:沒有參數,或參數都是默認值。一個類最多有一個缺省構造函數。
2、限制對象創建:將某些構造函數聲明為private型,從而限制通過某些構造函數創建對象。
3、拷貝構造函數:用一個類對象初始化該類的另一個對象。
拷貝構造函數:
拷貝構造函數就是用一個類對象初始化該類的另一個對象,但是拷貝構造函數的參數必須是同一個類的引用對象。
一個對象的拷貝構造函數可以訪問被拷貝的對象的私有成員,例如:
class Demo{
public:
Demo(){}
Demo(const Demo& demo):i(demo.i){"std::cout<<拷貝構造函數<<std::endl;"}//拷貝構造函數,參數必須是Demo&類型。
private:
int i;
}
類的數據成員初始化
類中的數據成員能否在定義類的時候就初始化?
存在問題,仍未解決~
class Demo{
public:
Demo(){}
private:
int i=0;//此處在定義類的時候初始化
}
答案是不能在此處初始化。原因在於:1、類的定義實際上相當於類型的聲明,里面的數據成員都是聲明,類型聲明並沒有分配內存空間,所以無法存放該類型;2、假如類是抽象類,那么在類中特征初始化將破壞類的抽象性。
- const成員:也就是常量成員,它們在聲明時就要初始化,因為它們為常量成員,一旦定義,就不能修改
- 引用成員&:引用代表的就是被引用者自身,是變量的別名,所以引用類型變量一旦初始化,它們也不能修改
- const、引用成員必須在類的構造函數中的初始化列表中初始化,不能在構造函數的函數體中進行賦值
- static成員:靜態成員,它不像其他成員,static沒有隱藏的this指針,所以static成員是屬於整個類的,不是某個對象的,靜態成員的初始化要在類外初始化,在類外初始化時需要隱藏static關鍵字,只有整形靜態常量可以在類內初始化
- 整型靜態常量:static const int 類型,這種數據類型可以在類內初始化
class A{
public:
const int a; // 常量成員,需要在構造函數的初始化列表中賦值初始化
int& b; // 引用成員,也是構造函數的初始化列表賦值
static int c; // 靜態成員,要在類外初始化
static const int d = 0; // 靜態整形,可以在類內初始化
static int f;
static const double e; // 只能在類外初始化
A(int a, int& b ):a(a), b(b){}
}
// 需要在類外初始化的成員變量,隱藏static關鍵字
const double A::e = 1.0;
const int A::f = 1; // 也可在類外初始化
類的析構函數
釋放在該類內申請的動態資源。
重載操作符和用戶定義的轉換
操作符重載
形式:返回值 operator操作符(參數列表){函數體}
注意:重載操作符的函數參數列表中必須至少有一個操作數為類類型。
例如:
/*將<<操作符重載為用以計算兩個int類型值之和,這種是錯誤的,因為兩個重載操作符的函數兩個參數都是非類的數據類型*/
int operator<<(int i,int j){
return i+j;
}
int main(){
int i=111,j=222;
int sum=i<<j;//相當於int sum=operator<<(i,j);
return 0;
}
/*將<<操作符重載為用以計算int變量和A類對象數據成員j的和,並返回int類型結果,這種是正確的,因為該操作符重載函數有一個參數為類類型*/
class A{
public:
A(int _j):j(_j){}
int j;
};
int operator<<(int i,A j){
return i+j.j;
}
int main(){
int i=111;
A a(222);
int sum=i<<a;//相當於int sum=operator<<(i,a);
cout<<sum<<endl;
return 0;
}
友元訪問在操作符重載的應用
友元訪問分為:
1、友元類訪問:將一個類聲明為另一個類的友元類。如:
class Demo1{}
class Demo{
friend class Demo1;
public:/*……*/
private:/*……*/
};
2、友元函數訪問:將一個函數聲明為另一個類的友元函數。如:
void f(){}
class Demo{
friend void f();
public:/*……*/
private:/*……*/
};
操作符重載分為幾種不同情況,對應幾種不同的操作符重載函數,需要不同的編程技巧:
情況一:重載操作符函數為全局函數,形式上是兩個操作數,函數參數也是兩個操作數,且兩個操作符可以為任意變量類型。
在這種情況下,當操作數有類類型時,在函數內如果想要直接訪問類內成員需要設置為該類的友元函數(當然也可以不訪問)。例如:
/*
重載+操作符,左邊為基本數據類型,右邊為類類型,返回值為類類型。
形式:類類型=基本數據類型+類類型;
*/
class Demo{
public:
friend Demo operator+(int i,Demo& j);//聲明操作符重載函數為類Demo的友元函數
Demo(int _i):i(_i){}
int getValue(){return i;}
private:
int i;
};
/*操作符重載函數*/
Demo operator+(int i,Demo& j){
Demo sum(i+j.i);//其實也未必需要將操作符重載函數設置為Demo類的友元函數,因為類的數據成員可以間接訪問,可以直接將sum(i+j.i)改為sum(i+j.getValue())即可。
return sum;
}
int main(){
int d1=111;
Demo d2(222);
Demo d3=d1+d2;//此時相當於調用了全局函數operator+(d1,d2),相當於將形式上兩個操作數作為兩個參數傳入了全局函數中。
cout<<d3.getValue()<<endl;
return 0;
}
情況二:重載操作符函數為成員函數,形式上是兩個操作數,函數參數是一個操作數,但是形式上的左操作數必須是重載操作符函數的類,右操作數可以為任意類型。
class Demo{
public:
Demo operator+(int i){//重載操作符函數為成員函數
Demo sum(this->i+i);
return sum;
}
Demo(int _i):i(_i){}
int getValue(){return i;}
private:
int i;
};
int main(){
int d1=111;
Demo d2(222);
Demo d3=d2+d1;/*相當於調用了Demo類的成員函數d2.operator+(d1),所以不能寫成d1+d2,否則會編譯錯誤。因為d1是int類型,而重載操作符函數是Demo的類成員函數 。形式上兩個操作數,實際上是一個操作數對象調用了成員函數,並將另一個操作數作為參數傳入。如果想要d1+d2合法,這時需要些一個全局函數來重載操作符+*/
return 0;
}
類模板
類模板的定義與實例化
模板類的定義形式:
template<class T1,typename T2,int size>
class Demo{
public:
void prin(){
for(int i=0;i<size;i++)std::cout<<t1[i]<<std::endl;
for(int i=0;i<size;i++)std::cout<<t2[i]<<std::endl;
}
private:
T1 t1[size];
T2 t2[size];
}
模板類型參數:用關鍵詞class或typename聲明。當實例化時會用數據類型標識符替換,例如T1替換成int。
模板非類型參數:用數據類型標識符聲明。當實例化時會用該數據類型的常量替換,例如size替換成int類型的常量10。
總結:模板類型參數相當於以數據類型為參數,通俗的說就是把數據類型當成類模板的變量;而模板非類型參數相當於以數據類型的常量表達式為參數,通俗來講就是把常量表達式的計算結果(常量表達式計算結果為常量)當成類模板的變量。
將模板類型參數與非類型參數替換成具體的數據類型與數據類型常量的過程,稱為模板實例化。
類模板的參數可以有缺省實參,這對類型參數和非類型參數都一樣。就像函數參數的缺省,模板參數的缺省是一個類型或一個值。如下示例:
template<class T1,class T2=string,int size=1024>//T1缺省為string類型,size缺省為計算結果為int類型的常量表達式1024
class Demo{
public:
Demo(T1 _t1,T2 _t2):t1(_t1),t2(_t2){}
void printValue(){
std::cout<<"t1="<<t1<<std::endl;
std::cout<<"t2="<<t2<<std::endl;
std::cout<<"size="<<size<<std::endl;
}
private:
T1 t1;
T2 t2;
};
int main(){
int i=1234;
Demo<string> demo("sss","mmm",i);//此處可以傳入變量i,是因為此處非類型參數以常量表達式的計算結果為參數。
demo.printValue();
return 0;
}
注意:在參數列表中,缺省參數右邊必須都是缺省參數,否則編譯器將報錯。
從通用的類模板定義生成類的過程被稱為模板實例化。
在模板實參的類型和非類型模板參數的類型之間允許進行一些轉換,能被允許的轉換集
是“函數實參上被允許的轉換”的子集:
1、左值轉換,包括從左值到右值的轉換、從數組到指針的轉換,以及從函數到指針的轉換。例如:
template <int *ptr> class Demo{};
int array[10];
Demo<array> demo;//從數組到指針的轉換
2、限定修飾符轉換。例如:
template <const int* ptr> class Demo{};
int i;
Demo<&i> demo;//從int*類型到const int*類型的轉換
3、提升。例如:
template <int i> class Demo{};
const short int ii;
Demo<ii> demo;//從short到int類型的提升
4、數值轉換。例如:
template <unsigned int size> Demo{};
Demo<1024> demo;//從int類型到unsigned int類型
類模板中的友元聲明
有三種友元聲明可以出現在類模板中:
1、非模板友元類或友元函數。友元類和友元函數都是非模板的。
例如:
class Foo {//非模板類
void bar();//非模板類的非模板成員函數
};
void foo();//非模板函數
template <class T>
class QueueItem {
friend class foobar;//非模板友元類
friend void foo();//非模板友元函數
friend void Foo::bar();//非模板友元類的非模板成員函數
// ...
};
2、綁定的友元類模板或函數模板。友元類和友元函數都是模板的,但是已經與實例化模板函數/類綁定,相當於該友元只能是某種實例化的模板函數/類。
template <class Type>
class foobar{ ... };//類模板
template <class Type>
void foo( QueueItem<Type> );//函數模板
template <class Type>
class Queue {
void bar();//類模板的成員函數模板
// ...
};
template <class Type>
class QueueItem {
friend class foobar<Type>;//綁定的模板友元類
friend void foo<Type>( QueueItem<Type> );//綁定的模板友元函數
friend void Queue<Type>::bar();//綁定的模板類友元成員函數
// ...
};
3、非綁定的友元類模板或函數模板
template <class Type>
class QueueItem {
template <class T>
friend class foobar;//非綁定的模板友元類
template <class T>
friend void foo( QueueItem<T> );//非綁定的模板友元函數
template <class T>
friend void Queue<T>::bar();//非綁定的模板類友元成員函數
// ...
};
注意:友元的目的是為了訪問友元聲明時所在類的私有數據成員,不改變該類的數據成員與成員函數的數量或其它。
類模板的靜態數據成員
略
======================================================================
C++面向對象
類繼承和子類型
基類和派生類的定義
基類必須被定義才能作為基類。例如:
class Parent;
class Child:public Parent{/*……*/};//錯誤,基類Parent必須被定義,只有聲明是無效的
前向聲明不能包含派生類的派生表。例如:
class Parent{};
class Child:public Parent;//錯誤,Child類為前向聲明,還未定義,前向聲明不能包含派生類的派生表
派生類的前向聲明正確的方法是:
class Parent{};
class Child;
基類的構造:
1、基類構造函數。如果有多個基類,則構造函數的調用順序是某類在類派生表中出現的順序,而不是它們在成員初始化表中的順序。
2、成員類對象構造函數。如果有多個成員類對象,則構造函數的調用順序是對象在類中,被聲明的順序,而不是它們出現在成員初始化表中的順序。
3、派生類構造函數。作為一般規則,派生類構造函數應該不能直接向一個基類數據成員賦值,而是把值傳遞,否則基類和派生類會變成“緊耦合”,當修改基類數據成員時會影響派生類。
基類與派生類構造函數:
class Parent{
public:
Parent(int i):_i(i){}
private:
int _i;
};
class Child:public Parent{
public:
Child(int i,int j):Parent(i),_j(j){}
private:
int _j;
};
還為了解的點:遲緩型錯誤檢測
繼承關系中類的構造函數與析構函數的調用順序
構造函數分為:父類的構造函數,父類數據成員的構造函數,子類的構造函數,子類數據成員的構造函數。
構造函數的調用順序為:
1、父類數據成員的構造函數;
2、父類的構造函數;
3、子類數據成員的構造函數;
4、子類的構造函數。
析構函數的調用順序完全相反。
另外,當父類或子類擁有多個數據成員時,數據成員構造函數的調用順序與父類或子類構造函數的成員初始化表的順序無關,而與類中定義該數據成員的順序有關。
例如:
class Demo{
public:
Demo(string s):_s(s){cout<<"Demo構造函數:"<<s<<endl;}
~Demo(){cout<<"Demo析構函數"<<_s<<endl;}
private:
string _s;
};
class Parent{
public:
Parent(int i):_i(i),d_parent2("d_parent2"),d_parent1("d_parent1"){cout<<"Parent構造函數"<<endl;}
~Parent(){cout<<"Parenet析構函數"<<endl;}
private:
int _i;
Demo d_parent1;
Demo d_parent2;
};
class Child:public Parent{
public:
Child(int i,int j):d_child2("d_child2"),Parent(i),_j(j),d_child1("d_child1"){cout<<"Child構造函數"<<endl;}
~Child(){cout<<"Child析構函數"<<endl;}
private:
int _j;
Demo d_child1;
Demo d_child2;
};
int main(){
Child c(1,2);
return 0;
}
運行結果是:
Demo構造函數:d_parent1
Demo構造函數:d_parent2
Parent構造函數
Demo構造函數:d_child1
Demo構造函數:d_child2
Child構造函數
Child析構函數
Demo析構函數d_child2
Demo析構函數d_child1
Parenet析構函數
Demo析構函數d_parent2
Demo析構函數d_parent1
Process returned 0 (0x0) execution time : 0.742 s
Press any key to continue.
基類與派生類的虛擬函數
缺省情況下,類的成員函數是非虛擬的(nonvirtual)。當一個成員函數為非虛擬的時候,通過一個類對象(指針或引用)而被調用的該成員函數,就是該類對象的靜態類型中定義的成員函數。例如:
class Parent{
public:
void demo(){cout<<"Parent"<<endl;}
};
class Child:public Parent{
public:
void demo(){cout<<"demo"<<endl;}
};
int main(){
Parent *p=new Child;//定義p的靜態類型為Parent類型,而分配內存的動態類型為Child
p->demo();//通過類對象的指針調用該函數
Parent &cp=*p;
cp.demo();//通過類對象的引用調用該函數
return 0;
}
此時的執行輸出結果為:
Parent
Parent
純虛函數:沒有函數體的函數聲明=0。例如:virtual void demo()=0;
虛函數可以通過動態聯編動態調用,也可以靜態調用,例如:
class Parent{
public:
virtual void demo(){cout<<"Parent"<<endl;}
};
class Child:public Parent{
public:
void demo(){cout<<"demo"<<endl;}
};
int main(){
Parent *p=new Child;
p->Parent::demo();//虛擬函數的靜態調用,在編譯時刻解析,調用的類型為定義指針的類型
p->demo();//虛擬函數的動態調用,在運行時刻動態聯編,調用的類型為指針所指向地址的對象類型
return 0;
}
虛析構函數
當我們通過基類的指針來銷毀對象,如果析構函數不為虛的話,就不能正確識別對象類型,從而不能正確銷毀對象。
例如:
/*基類的析構函數沒有被聲明為虛函數時*/
class Parent{
public:
Parent(){cout<<"Parent構造函數"<<endl;}
~Parent(){cout<<"Parent析構函數"<<endl;}
};
class Child:public Parent{
public:
Child(){cout<<"Child構造函數"<<endl;}
~Child(){cout<<"Child析構函數"<<endl;}
};
int main(){
Parent* p=new Child;
delete p;//通過基類的指針來銷毀對象,此時父類的析構函數不是虛函數
return 0;
}
運行結果為:
Parent構造函數
Child構造函數
Parent析構函數
Process returned 0 (0x0) execution time : 0.647 s
Press any key to continue.
/*基類的析構函數被聲明為虛函數時*/
class Parent{
public:
Parent(){cout<<"Parent構造函數"<<endl;}
virtual ~Parent(){cout<<"Parent析構函數"<<endl;}
};
class Child:public Parent{
public:
Child(){cout<<"Child構造函數"<<endl;}
~Child(){cout<<"Child析構函數"<<endl;}
};
int main(){
Parent* p=new Child;
delete p;//通過基類的指針來銷毀對象,此時父類的析構函數是虛函數
return 0;
}
運行結果為:
Parent構造函數
Child構造函數
Child析構函數
Parent析構函數
Process returned 0 (0x0) execution time : 0.485 s
Press any key to continue.
可以得出結論,將基類的析構函數聲明為虛函數常常是必要的,否則銷毀對象時可能會導致析構不完整。