面向對象篇
1 面向對象與面向過程的含義以及區別?
面向對象
面向對象是把數據及對數據的操作方法放在一起,作為一個相互依存的整體,即對象。對同類對象抽象出其共性,即類,類中的大多數數據,只能被本類的方法進行處理。類通過一些簡單的外部接口與外界發生關系,對象與對象之間通過消息進行通信。程序流程由用戶在使用中決定。例如,站在抽象的角度,人類具有身高、體重、年齡、血型等一些特性。人類僅僅只是一個抽象的概念,它是不存在的實體,但是所有具備人類這個群體的屬性與方法的對象都叫人,這個對象人是實際存在的實體,每個人都是人這個類的一個對象。
面向過程
面向過程是一種以事件為中心的開發方法,就是自頂向下順序執行,逐步求精,其程序結構是按功能劃分為若干個基本模塊,這些模塊形成一個樹狀結構,各模塊之間的關系也比較簡單,在功能上相對獨立,每一模塊內部一般都是由順序、選擇和循環三種基本結構組成的,其模塊化實現的具體方法是使用子程序,而程序流程在寫程序時就已經決定。例如五子棋,面向過程的設計思路就是首先分析問題的步驟:第一步,開始游戲;第二步,黑子先走;第三步,繪制畫面;第四步, 判斷輸贏;第五步,輪到白子;第六步,繪制畫面;第七步,判斷輸贏;第八步,返回步驟2;第九步,輸出最后結果。把上面每個步驟用分別的函數來實現,就是一個面向過程的開發方法。
區別
(1) 出發點不同。
面向對象是用符合常規思維方式來處理客觀世界的問題,強調把問題的要領直接映射到對象及對象之間的接口上。而面向過程方法則不然,它強調的是過程的抽象化與模塊化,它是以過程為中心構造或處理客觀世界問題的。
(2) 層次邏輯關系不同。
面向對象方法則是用計算機邏輯來模擬客觀世界中的物理存在,以對象的集合類作為處理問題的基本單位,用類的層次結構來體現類之間的繼承和發展。而面向過程方法處理問題的基本單位是能清晰準確地表達過程的模塊,用模塊的層次結構概括模塊或模塊間的關系與功能,把客觀世界的問題抽象成計算機可以處理的過程。
(3) 數據處理方式與控制程序方式不同。
面向對象方法將數據與對應的代碼封裝成一個整體,原則上其他對象不能直接修改其數據,即對象的修改只能由自身的成員函數完成。控制程序方式上是通過“事件驅動”來激活和運行程序。而面向過程方法是直接通過程序來處理數據,處理完畢后即可顯示處理結果。在控制程序方式上是按照設計調用或返回程序,不能自由導航,各模塊之間存在著控制與被控制、 調用與被調用的關系。
(4) 分析設計與編碼轉換方式不同。
面向對象方法貫穿軟件生命周期的分析、設計及編碼之間,是一種平滑過程,從分析到設計再到編碼采用一致性的模型表示,即實現的是一種無縫連接。而面向過程方法強調分析、設計及編碼之間按規則進行轉換,貫穿軟件生命周期的分析、設計及編碼之間,實現的是一種有縫的連接。
2 面向對象的基本特征有哪些?
面向對象方法首先對需求進行合理分層,然后構建相對獨立的業務模塊,最后通過整合各模塊,達到高內聚、低耦合的效果,從而滿足客戶要求。具體而言,它有3個基本特征:封裝、繼承和多態。
(1) 封裝是指將客觀事物抽象成類,每個類對自身的數據和方法實行保護。類可以把自己的數據和方法只讓可信的類或對象操作,對不可信的進行隱藏。C++中類是一種封裝手段,采用類來描述客觀事物的過程就是封裝,本質上是對客觀事物的抽象。
(2) 繼承可以使用現有類的所有功能,而不需要重新編寫原來的類,它的目的是為了進行代碼復用和支持多態。它一般有3種形式:實現繼承、可視繼承、接口繼承。其中,實現繼承 是指使用基類的屬性和方法而無需額外編碼的能力;可視繼承是指子窗體使用父窗體的外觀和實現代碼;接口繼承僅使用屬性和方法,實現滯后到子類實現。前兩種(類繼承)和后一種 (對象組合=>接口繼承以及純虛函數)構成了功能復用的兩種方式。
(3) 多態是指同一個實體同時具有多種形式,它主要體現在類的繼承體系中,它是將父對象設置成為和一個或更多的它的子對象相等的技術,賦值以后,父對象就可以根據當前賦值給它的子對象的特性以不同的方式運作。簡單地說,就是允許將子類類型的指針賦值給父類類型的指針。編譯時多態是靜態多態,在編譯時就可以確定對象使用的形式。
3 什么是深拷貝?什么是淺拷貝?
如果一個類擁有資源(堆或者是其他系統資源),當這個類的對象發生復制過程時,資源重新分配,這個過程就是深拷貝;反之對象存在資源,但復制過程并未復制資源的情況視為淺拷貝。
例如,在某些狀況下,類內成員變量需要動態開辟堆內存,如果實行位復制,也就是把對象里的值完全復制給另一個對象,如A=B,這時,如果類B中有一個成員變量指針已經申請了內存,那么類A中的那個成員變量也指向同一塊內存。這就出現了問題:當B把內存釋放 了,如通過析構函數,這時A內的指針就變成野指針了,導致運行錯誤。
深復制的程序示例如下:
#include <iostream> using namespace std; class CA
{
public:CA(int b,char* cstr);CA(const CA& C); void Show();?CA();
private: int a; char *str;
};CA::CA(int b,char* cstr)
{a=b;str=new char[b]; strcpy(str,cstr);
}CA::CA(const CA& C)
{a=C.a;str=new char[a]; //給str重新分配內存空間,所以為深拷貝 if(str!=0)strcpy(str,C.str);
}void CA::Show()
{cout<<str<<endl;
}CA::?CA()
{delete str;
}int main()
{CA A(10,"Hello"); CA B=A;?B.Show(); return 0;
}
程序輸出結果:
Hello
如果沒有自定義復制構造函數時, 系統將會提供給一個默認的復制構造函數來完成這個過程,就會造成“淺拷貝”。所以要自定義賦值構造函數,重新分配內存資源,實現“深拷貝”。
4 什么是友元?
類具有封裝、繼承、多態、信息隱藏的特性,只有類的成員函數才可以訪問類的標記為 private的私有成員,非成員函數可以訪問類中的公有成員,但是卻無法訪問私有成員,為了使非成員函數可以訪問類的成員,唯一的做法就是將成員都定義為public,但如果將數據成員都定義為公有的,這又破壞了信息隱藏的特性。友元正好解決了這一棘手的問題。在使用友元函數時,一般需要注意以下幾個方面的問題:
(1) 必須在類的說明中說明友元函數,說明時以關鍵字friend開頭,后跟友元函數的函數原型,友元函數的說明可以出現在類的任何地方,包括private和public部分。
(2) 友元函數不是類的成員函數,所以友元函數的實現與普通函數一樣,在實現時不用 “::”指示屬于哪個類,只有成員函數才使用“::”作用域符號。
(3) 友元函數不能直接訪問類的成員,只能訪問對象成員。
(4) 調用友元函數時,在實際參數中需要指出要訪問的對象。
(6) 類與類之間的友元關系不能繼承。
友元一般定義在類的外部,但它需要在類體內進行說明,為了與該類的成員函數加以區別,在說明時前面加以關鍵字friend。需要注意的是,友元函數不是成員函數,但是它可以訪問類中的私有成員。友元的作用在于提高程序的運行效率,但是它破壞了類的封裝性和隱藏 性,使得非成員函數可以訪問類的私有成員。
如下為一個友元函數的例子:
#include <iostream>
#include <string>
using namespace std; class Fruit
{
public:Fruit(const string &nst="apple",const string &cst="green"):name(nst),colour(cst){ }?Fruit(){}friend istream& operator>>(istream&,Fruit&); friend ostream& operator<<(ostream&,const Fruit&); void print(){cout<<colour<<" "<<name<<endl;}private:string name; string colour;
};ostream& operator<<(ostream &out,const Fruit &s) //重載輸出操作符
{out<<s.colour<<" "<<s.name; return out;
}istream& operator>>(istream& in,Fruit &s) //重載輸入操作符
{in>>s.co1our>>s.name; if(!in)cerr<<"Wrong input!"<<endl; return in;
}int main()
{Fruit apple; cin>>apple; cout<<apple; return 0;
}
5 類的成員變量的初始化順序是按照聲明順序嗎?
在C++中,類的成員變量的初始化順序只與變量在類中的聲明順序有關,與在構造函數中的初始化列表的順序無關。而且靜態成員變量先于實例變量,父類成員變量先于子類成員變量。
示例程序如下:
class Test
{
private :int nl; int n2;
public:Test();
};Test::Test():n2(2),nl(1)
{}
當查看相關匯編代碼時,就能看到正確的初始化順序了。因為成員變量的初始化次序跟變量在內存中的次序有關,而內存中的排列順序早在編譯期就根據變量的定義次序決定了。
從全局看,變量的初始化順序如下:
(1) 基類的靜態變量或全局變量。
(2) 派生類的靜態變量或全局變量。
(3) 基類的成員變量。
(4) 派生類的成員變量。
6 一個類為另一個類的成員變量時,如何對其進行初始化?
示例程序如下:
class ABC
{
public:ABC(int x, int y, int z);
private : int a; int b; int c;
};class MyClass?
{
public:MyClass():abc(1,2,3){} private:
ABC abc;
};
上例中,因為ABC有了顯式的帶參數的構造函數,那么它是無法依靠編譯器生成無參構造函數的,所以必須使用初始化列表:abc(1,2,3),才能構造ABC的對象。
7 C++中的空類默認產生哪些成員函數?
C++中空類默認會產生以下6個函數:默認構造函數、復制構造函數、析構函數、賦值運算符重載函數、取址運算法重載函數、const取址運算符重載函數等。
class Empty
{
public:Empty();//默認構造函數Empty( const Empty& );//復制構造函數?Empty();//析構函數Empty& operator=(const Empty&);// 賦值運算符重載函數Empty* operator&();// 取址運算重載函數const Empty* operator&( ) const; // const取址運算符重載函數
};
8 C++提供默認參數的函數嗎?
C++可以給函數定義默認參數值。在函數調用時沒有指定與形參相對應的實參時,就自動使用默認參數。
默認參數的語法與使用:
(1) 在函數聲明或定義時,直接對參數賦值,這就是默認參數。
(2) 在函數調用時,省略部分或全部參數。這時可以用默認參數來代替。
通常調用函數時,要為函數的每個參數給定對應的實參。例如:
void delay(int loops=1000);//函數聲明 void delay(int loops) //函數定義
{if(loops==0){return;}for(int i=0;i<loops;i++);
}
在上例中,如果將delay()函數中的loops定義成默認值1000,這樣,以后無論何時調用delay()函數,都不用給loops賦值,程序都會自動將它當做值 1000進行處理。例如,當執行delay(2500)調用時,loops的參數值為顯性化的,被設置為 2500;當執行delay()時,loops將采用默認值1000。
?
默認參數在函數聲明中提供,當有聲明又有定義時,定義中不允許默認參數。如果函數只有定義,則默認參數才可出現在函數定義中。例如:
oid point(int=3,int=4);//聲明中給出默認值 void point(int x,int y) //定義中不允許再給出默認值
{cout<<x<<endl;cout<<y<<endl;
}
?
如果一組重載函數(可能帶有默認參數)都允許相同實參個數的調用,將會引起調用的二義性。例如:
void func(int);//重載函數之一
void func(int,int=4);//重載函數之二,帶有默認參數
void func(int=3,int=4);//重載函數三,帶有默認參數
func(7);//錯誤:到底調用3個重載函數中的哪個?
func(20,30);//錯誤:到底調用后面兩個重載函數的哪個?
9 什么是虛函數及其作用?
指向基類的指針在操作它的多態類對象時,可以根據指向的不同類對象調用其相應的函數,這個函數就是虛函數。
虛函數的作用:在基類定義了虛函數后,可以在派生類中對虛函數進行重新定義,并且可以通過基類指針或引用,在程序的運行階段動態地選擇調用基類和不同派生類中的同名函數。(如果在派生類中沒有對虛函數重新定義,則它繼承其基類的虛函數。)
下面是一個虛函數的實例程序:
#include "stdafx.h"
#include<iostream>
using namespace std;class Base
{
public:virtual void Print()//父類虛函數{printf("This is Class Base!\n");}
};class Derived1 :public Base
{
public:void Print()//子類1虛函數{printf("This is Class Derived1!\n");}
};class Derived2 :public Base
{
public:void Print()//子類2虛函數{printf("This is Class Derived2!\n");}
};int main()
{Base Cbase;Derived1 Cderived1;Derived2 Cderived2;Cbase.Print();Cderived1.Print();Cderived2.Print();cout << "---------------" << endl;Base *p1 = &Cbase;Base *p2 = &Cderived1;Base *p3 = &Cderived2;p1->Print();p2->Print();p3->Print();
}/*
輸出結果:This is Class Base!
This is Class Derived1!
This is Class Derived2!
---------------
This is Class Base!
This is Class Derived1!
This is Class Derived2!
*/
需要注意的是,虛函數雖然非常好用,但是在使用虛函數時,并非所有的函數都需要定義成虛函數,因為實現虛函數是有代價的。在使用虛函數時,需要注意以下幾個方面的內容:
(1) 只需要在聲明函數的類體中使用關鍵字virtual將函數聲明為虛函數,而定義函數時不需要使用關鍵字virtual。
(2) 當將基類中的某一成員函數聲明為虛函數后,派生類中的同名函數自動成為虛函數。
(3) 非類的成員函數不能定義為虛函數,全局函數以及類的成員函數中靜態成員函數和構造函數也不能定義為虛函數,但可以將析構函數定義為虛函數。
(4) 基類的析構函數應該定義為虛函數,否則會造成內存泄漏。基類析構函數未聲明virtual,基類指針指向派生類時,delete指針不調用派生類析構函數。有 virtual,則先調用派生類析構再調用基類析構。
10 C++中如何阻止一個類被實例化?
C++中可以通過使用抽象類,或者將構造函數聲明為private阻止一個類被實例化。抽象類之所以不能被實例化,是因為抽象類不能代表一類具體的事物,它是對多種具有相似性的具體事物的共同特征的一種抽象。例如,動物作為一個基類可以派生出老虎、孔雀等子類,但是動物本身生成對象不合情理。
11 多態類中的虛函數表是 Compile-Time,還是 Run-Time 時建立的?
虛擬函數表是在編譯期就建立了,各個虛擬函數這時被組織成了一個虛擬函數的入口地址的數組.而對象的隱藏成員--虛擬函數表指針是在運行期--也就是構造函數被調用時進行初始化的,這是實現多態的關鍵。
12 多態的作用?
主要是兩個:
- 隱藏實現細節,使得代碼能夠模塊化;擴展代碼模塊,實現代碼重用;
- 接口重用:為了類在繼承和派生的時候,保證使用家族中任一類的實例的某一屬性時的正確調用。
13 C++函數中那些不可以被聲明為虛函數 ?
常見的不能聲明為虛函數的有:普通函數(非成員函數);靜態成員函數;內聯成員函數;構造函數;友元函數。
1.為什么C++不支持普通函數為虛函數?
普通函數(非成員函數)只能被overload,不能被override,聲明為虛函數也沒有什么意思,因此編譯器會在編譯時邦定函數。
2.為什么C++不支持構造函數為虛函數?
這個原因很簡單,主要是從語義上考慮,所以不支持。因為構造函數本來就是為了明確初始化對象成員才產生的,然而virtual function主要是為了再不完全了解細節的情況下也能正確處理對象。另外,virtual函數是在不同類型的對象產生不同的動作,現在對象還沒有產生,如何使用virtual函數來完成你想完成的動作。(這不就是典型的悖論)
3.為什么C++不支持內聯成員函數為虛函數?
其實很簡單,那內聯函數就是為了在代碼中直接展開,減少函數調用花費的代價,虛函數是為了在繼承后對象能夠準確的執行自己的動作,這是不可能統一的。(再說了,inline函數在編譯時被展開,虛函數在運行時才能動態的邦定函數)
4.為什么C++不支持靜態成員函數為虛函數?
這也很簡單,靜態成員函數對于每個類來說只有一份代碼,所有的對象都共享這一份代碼,他也沒有要動態邦定的必要性。
5.為什么C++不支持友元函數為虛函數?
因為C++不支持友元函數的繼承,對于沒有繼承特性的函數沒有虛函數的說法。
14 如何修改類和排序函數,根據age對傳入的類數組進行排序?
類使用大于運算符(>)重載,排序函數定義為模板函數,如下所示。
#include <iostream>
#include <string>using namespace std;class Student
{
public:Student(){}void SetStu(int t_len, string t_name){age = t_len;name = t_name;}void Print(){cout << "age:" << age << " name:" << name << endl;}bool operator >(const Student &another){if (age > another.age)return true;elsereturn false;}private:int age;string name;
};template <typename T>
//冒泡排序法 - 升序
void BubbleSort(T arr[], int len)
{T temp;int i, j;for (i = 0; i < len-1; i++){for (j = 0; j < len-i-1; j++){if (arr[j] > arr[j + 1]){temp = arr[j + 1];arr[j + 1] = arr[j];arr[j] = temp;}}}
}// 程序的主函數
int main()
{Student arr[5];arr[0].SetStu(2, "zhangSan");arr[1].SetStu(1, "liSi");arr[2].SetStu(3, "wangWu");arr[3].SetStu(5, "xiaoMing");arr[4].SetStu(4, "xiaoHong");BubbleSort(arr, 5);for (int i = 0; i < 5; i++)arr[i].Print();return 0;
}/*
輸出結果:age:1 name:liSi
age:2 name:zhangSan
age:3 name:wangWu
age:4 name:xiaoHong
age:5 name:xiaoMing
*/
一些需要注意的要點:
- 基類的構造函數/析構函數都不能被派生類繼承。
- 《C++程序設計語言》,將多態分為兩類,虛函數提供的東西稱作運行時多態,模板提供的為編譯時多態和參數式多態。
- 訪問屬性為private的基類成員,則不能被派生類繼承。
- C++中的結構體也必須使用new 創建。
- C++中結構體可以定義成員函數,也可以繼承,另外類也可以由結構繼承而來。