《C++ 習題與解析》筆記


C++基礎: 基礎數據類型,簡單輸入輸出,流程控制語句,函數與編譯預處理,數組,結構體,指針與引用
C++面向對象部分: 類與對象,構造和析構函數,繼承與派生,友元,虛函數,靜態成員,運算符重載

Chapter-1 C++語言概述

位運算操作符
單目:~(按位求反)
雙目:&(與)、 |(或)、 ^(按位異或)
移位運算符

<< (左移): 左移是將一個二進制數按指定的位數向左移位,移掉的位被丟棄,右邊移出的空位一律補0
>> (右移): 右移是將一個二進制數按指定的位數向右移位,移掉的位被丟棄,左邊移出的空位一律補0,或補符號位

逗號運算符

d1, d2, d3, d4: 計算一個逗號表達式的值時,從左至右依次計算各個表達式的值,最后計算的一個表達式的值和類型便是整個逗號表達式的值和類型

二維數組指針表示
	//輸出對應的值的三種方法
    int b[2][3]; 
    a. *(*(b+i)+j)
    b. *(b[i]+j)
    c. *(&b[0][0]+3*i+j)
const 位置與指向問題
	//都定義了a是一個常整數,不能修改a的值
	int const a = 1;
	const int a = 1;

	int b[] = {1,2,3}
	const int *a = b; // a為指向常整數的指針,a為數組b的首地址,a指向的整數不可修改,不能通過a來修改其指向的值,但可以通過b來修改數組值,而且a可以指向其他數組
	int * const a = b; // a為指向整數的常指針,a為數組b的首地址,a指向的整數可以修改,但是指針a不可修改,不能讓a指向其他數組,也不能執行a++語句
指針傳址操作
void swap()
{
	int a = 1,b=2;
	int& a1 = a;
	int& b1 = b;
	swap1(a,b); //傳變量的值(在方法內操作的只是temp,改變不了實際ab的值) 
	swap2(&a,&b); //傳變量的地址(直接操作內存地址的值,最粗暴的方式 ) 
	swap3(a1,b1); //傳變量的引用 (變量與變量的別名,它們的地址是同一個,直接操作ab的值 ) 
	cout<< a << "," << b; 
}

void pointer2(){
    int  a=10;
    int& b =a;
    int  *q;
    q = &a;
    
    //修改a的值 
    *q = 2;
    printf("%d\n",*q);
    changeValue(q);//不行
    changeValue2(&q);//(ok)傳二級指針修改值 
    changeValue3(b);//(ok)直接傳引用修改值 
    printf("%d\n",*q);
}
void changeValue(int *p){//傳進來的p是a的地址,或者說p只是一個普通變量,這個變量存放了a的地址 
    int  b=100;
    p = &b;//因為p是一個變量,所以,這邊的p是個副本_p,對副本進行操作,並不會改變實際a的值 
}
void changeValue2(int **p){//傳進來的是p的地址,不是a的地址!! 
    int  b=100;
    *p = &b;//這邊雖然也有一個副本_p,但是沒有影響,因為*p取到p地址的值,也就是a的地址,然后修改其值 
}
void changeValue3(int& p){
    p = 100;//引用理解為是對指針進行了封裝,使用起來簡單 
}

Chapter-1 C++語言概述(錯題)


  • [例1.3]
   int i=1;
   void max()
   { int i = i;} 
   //main()里的i是未定義值,是局部變量,當執行int i=i;時,先在內存中為局部變量i分配內存空間,其值是不確定的,然后執行i=i,由於值不確定,所以main()里的i是一個未定義值
  • [例1.10]
    在一定條件下,指針可以進行兩種運算,即兩個指針可以相減,一個指針可以與一個適當的整數相加減,但不能進行兩個指針的相加運算

  • [例1.16]

	struct
	{
		float f; //4 byte
		char c;  //1 byte
		int adf[3]; //12byte;
	}x;
	cout << sizeof(x) << endl; // output: 20byte;
	//默認以4字節為對齊單位,f占4個字節,c占一個字節,adf占12個字節,總共17個字節。按4字節為對齊單位時,要選擇4的倍數,即為20個字節。
	
  • [例1.24]
    swicth(){ case _A_: 表達式。 } //在A處:case之后只能用常量表達式,不能用實型表達式
  • [例1.30]
 for(int i=0,j=10; i=j=10; i++,j--); //執行次數:無限次。 return i=j=10 -> true
  • [例1.36]
    若要定義一個只允許本源文件所有函數使用的全局變量,應該使用的存儲類別是:static
    c++規定,全局變量分為兩種:extern和static型,前者的作用域為整個程序,后者的作用域為定義該變量的文件
  • [例1.43]
	void main()
	{
		int b = 3;
		int arr[] = {6,7,8,9,10};
		int *ptr = arr;
		*(ptr++) += 123;
		printf(" %d, %d\n",*ptr,*(++ptr));  //output: 8 8 ;
	}

對於printf語句,其參數從右向左運算,第一個是*(++ptr),第二個是*ptr

  • [例1.57]
    func(x)的功能是將x轉化為2進制,求其中含有的1的個數
	int func(int x)
	{
		int countx = 0;
		while(x)
		{
			countx++;
			x = x & (x-1);    //每一次計算對應2進制的一個1就會變成0 直到所有1都變為0循環結束
		}
		return countx;
	}

Chapter-2 類和對象


定義類注意點
  1. 類體中不允許對所定義的數據成員進行初始化
  2. 用new創建的類對象都是匿名對象,必須用一個指針指向它,通過該指針對這個對象進行操作 class A{..}; A *p = new A;
拷貝構造函數

用一個已知對象來初始化一個被創建的同類對象

class Sample{
	Sample(const Sample &s){...}
}
void main(){
	Sample s1, s2(s1);
}

使用情況如下:

  • 明確表示由一個對象初始化另一個對象時
  • 當對象作為函數實參傳遞給函數形參時
  • 當對象作為函數返回值時
調用析構函數

析構函數自動調用的情況

  • 對象被定義在一個函數體內,則當這個函數結束時,該對象的析構函數被自動調用
  • 使用new運算符動態創建一個對象后,當使用delete運算符釋放它時,delete將會自動調用析構函數

析構函數的調用順序與構造函數相反

	void main(){
		Sample s1,s2(10); //析構函數調用時先釋放s2,再釋放s1
	}
深拷貝/淺拷貝

淺拷貝:當兩個對象之間進行復制,復制完成后,還共享某些資源(內存空間),其中一個對象銷毀會影響另一個對象,這種對象之間的復制稱為對象淺復制

class Sample{
private:	
	int no;
	char *pname;
public:
	Sample(const Sample &s){
		no = s.no;
		pname = s.pname;
	}
}
void main(){
	Sample s1, s2(s1);
}

深拷貝:當兩個對象之間進行復制,復制完成后,他們不會共享任何資源,其中一個對象的銷毀不會影響另一個對象
種對象之間的復制稱為對象深復制

class Sample{
private:	
	int no;
	char *pname;
public:
	Sample(const Sample &s){
		no = s.no;
		// pname = s.pname;
		pname = new char[strlen(s.pname)+1];
		strcpy(pname,s.pname);
	}
}
void main(){
	Sample s1, s2(s1);
}
類成員函數指針
class Sample
{
	int m,n;
public:	
	void setm(int i){  m=i; }
	void setn(int i){  n=i; }
}
void main(){
	void(Sample::*pfun)(int);   //類成員函數指針
	Sample a;
	pfun=Sample::setm;          //指向Sample類成員函數setm;
	(a.*pfun)(10);
	pfun=Sample::setn;          //指向Sample類成員函數setn;
	(a.*pfun)(20);
}

程序中類Sample的setm/setn成員函數必須具有相同的返回類型(這里均為void),而且為public時才能這樣使用

子對象

構造函數的執行次序是:(析構函數相反)

  1. 以子對象在類中說明的順序調用子對象初始化列表中列出的各構造函數A(參數表):obj(參數表2){ 函數體; }
  2. 然后執行函數體
class B1{
public:
	B1(){cout << "B1:Constructor" << endl; }
	~B1(){cout << "B1:Constructor" << endl; }
};
class B2{
public:
	B2(){cout << "B2:Constructor" << endl; }
	~B2(){cout << "B2:Constructor" << endl; }
};
class B3{
public:
	B3(){cout << "B3:Constructor" << endl; }
	~B3(){cout << "B3:Constructor" << endl; }
};

class A
{
	B1 b1;
	B2 b2;
	B3 b3;
pubilc:
	A():b3(),b2(),b1(){ cout<< "A:Constructor" << endl; }
	~A() { cout << "A:Destructor" << endl; }
};

void main()
{
	A a;
}

//Output
B1:Constructor
B2:Constructor
B3:Constructor
A:Constructor
B3:Destructor
B2:Destructor
B1:Destructor
常類型
  1. 常對象:
類名 const 對象名 / const 類名 對象名
  • 聲明為常對象的同時必須被初始化,並從此不能改寫對象的數據成員。
  • 常對象只能調用類的常成員函數或類的靜態成員函數

const Sample a(10);

  1. 常對象成員:
    1. 常成員函數
      數據類型 函數名(參數表) const;
  • 常成員函數不更新對象的數據成員,也不能調用該類中沒有用const修飾的成員函數
  • 如果將一個對象聲明為常對象,則通過該常對象只能調用它的常成員函數,而不能調用其他成員函數
  • const關鍵字可以被用於參與對重載函數的區分:
    	void print();
    	void print() const; //正確的重載
    
    1. 常對象成員:
      只能通過初始化列表對該數據成員進行初始化
    class Sample
    {
    	const int n;
    public:
    	Sample(int i):n(i){}
    }
    

Chapter-2 類和對象(錯題)


  • [例2.32]
    已知f1(int)是類A的公有成員函數,p是指向成員函數f1的指針,為其賦值時正確的是:p=A::f1

  • [例2.35]

    class Test
    {
    public:
    	Test(){}
    	~Test(){cout << '#'; }
    };
    
    void main(){ 
    	Test temp[2], *pTemp[2];   // 執行程序輸出:2個'#'號
    }
    

    Test *pTemp[2]只建立了兩個Test對象指針的數組,並沒有創建Test對象,不會調用構造函數和析構函數

  • [例2.37]

    class point
    {
    public:
    	point() { cout<<"C"; }
    	~point() { cout<<"D"; }
    };
    
    void main()
    {
    	point *ptr;
    	point A,B;
    	point *ptr_point = new point[3];
    	//output: CCCCCDD
    }
    

    沒有delete ptr_point 因此沒有調用對應的析構函數

  • [例2.55]

class B
{
	int x,y;
public:
	B() {x=y=0; cout << "Constructor1" << endl; }
	B(int i) {x=i;y=0; cout << "Constructor2" << endl; }
	B(int i, int j) {x=i;y=j; cout << "Constructor3" << endl; }
	~B(){ cout<<"Destructor" << endl; }
	void print()
	{ cout << "x=" << ",y=" << y << endl;}
}

void main()
{
	B *ptr;
	ptr = new B[3];
	ptr[0] = B();
	ptr[1] = B(5);
	ptr[2] = B(2,3);
	for(int i=0;i<3;i++)
		ptr[i].print();
	delete[] ptr;
}

//output:
Constructor1
Constructor1
Constructor1
Constructor1
Destructor
Constructor2
Destructor
Constructor3
Destructor
x=0,y=0
x=5,y=0
x=2,y=3
Destructor
Destructor
Destructor

Chapter-3 引用


引用的概念
  • 引用不是變量,引用必須初始化

  • 引用不是值,不占存儲空間,引用只有申明,沒有定義

  • 引用只在聲明時帶有&,以后就像普通變量一樣使用,不能再帶&

  • 指針變量也可引用

    void main()
    {
    	int n=10, *pn=&n, *&rn=pn;
    	(*pn)++;   //n=11
    	(*rn)++;   //n=12
    }
    
  • void的引用是不允許的

  • 不能建立引用的數組

  • 沒有引用的引用

  • 沒有空引用

引用作為函數參數
class Sample{...};
void fun(Sample s1,Sample &s2)
{
	s1.setxy(12,18);   //不能對目標對象操作
 	s2.setxy(23,15);   //能對目標對象操作
}
常引用

常引用往往用作函數的形參,這樣該函數中並不能更新該參數所引用的對象,從而保護實參不被修改

	int x = 2;
	const int &n = x;
	
	n++; //錯誤,不能通過常引用更新對象,但執行x++是正確的
引用和指針的區別

兩者不同點:

  • 指針是個實體,而引用僅是個別名;引用不是變量,引用必須初始化,指針是變量,可以不用初始化。
  • 指針是變量,可以不初始化。
  • 引用不是值,不占存儲空間,而指針變量會占用存儲空間

Chapter-4 友元函數


友元函數概念
  • 友元函數是一種能夠訪問類中的私有成員的非成員函數,提高了程序的運行效率,破壞了類的封裝性和隱藏性,使得非成員函數可以訪問類的私有成員
  • 友元函數可以是多個類的友元friend double dist(Line l, Point p);
  • 友元關系不能被繼承
  • 友元關系是單向的,不具有交換性。若類B是類A的友元,類A不一定是類B的友元
友元類
class A
{
	...
public:
	friend class B;
	...
}

當一個類作為另一個類的友元時,意味着這個類的所有成員函數(B)都是另一個類的友元函數(A),可以調用另一個類的私有變量(A)

Chapter-5 運算符重載


重載為類的成員函數 類名 operator 運算符(參數表)
class Point
{
	int x,y;
public:
	Point(){}
	Point(int i, int j) {}
	Point operator+(Point &p){ return Point(x+p.x,y+p.y); }
};
重載為類的友元函數
class Point
{
	int x,y;
public:
	Point(){}
	Point(int i, int j) {}
	friend Point operator+(Point &p1, Point &p2)
	{ return Point(p1.x+p2.x,p1.y+p2.y); }
};
  1. 一般情況下,單目運算符最好重載為類的成員函數;雙目運算符則最好重載為類的友元函數(=、()、[]、-> 不能重載為類的友元函數
  2. 當需要重載運算符具有可交換性時,選擇重載為友元函數
其他運算符重載
  • 一元自加/自減運算符重載
    class Number
    {
    	int x;
    public:
    	Number(){ x=0; }
    	Number(int i){ x=i; }
    	void disp(){ cout<< "x=" << x << endl;}
    	void operator++() {x++;}    //前置運算符
    	void operator++(int) {x+=2;}//后置運算符
    }
    
    void main()
    {
    	Number obj(5);
    	obj.disp();
    	++obj;
    	obj.disp();
    	obj++;
    	obj.disp();
    }
    
    //output:
    x=5
    x=6
    x=8
    
  • 算術賦值運算符重載
    class Vector
    {
    	int x,y;
    public:
    	void operator+=(Vector D) { x+=D.x; y+=D.y; }
    	void operator-=(Vector D) { x-=D.x; y-=D.y; }
    }
    
  • 關系運算符重載
    class Rect
    {
    	int length,width;
    public:
    	friend int operator>(Rect r1, Rect r2)
    	{ return r1.length*r1.width > r2.length*r2.width?1:0 }
    }
    

Chapter-6 繼承與派生


Alt text

單繼承派生類的構造函數調用順序
1. 基類的構造函數
2. 子對象類的構造函數(如果有的話)
3. 派生類構造函數

當基類的構造函數使用一個或多個參數時,則派生類必須定義構造函數,提供將參數傳遞給基類構造函數的途徑。基類中有默認的構造函數或者根本沒有定義構造函數時,派生類不必負責調用積累的構造函數

  • 基類指針引用一個派生類的對象。這種引用方式是安全的,但這種方法只能引用基類成員
  • 派生類指針引用基類的對象,這種引用方式錯誤
多繼承派生類的構造函數調用順序
1. 調用基類的構造函數,調用次序按照它們被繼承時聲明的次序(從左向右)
2. 子對象類的構造函數,調用次序按照它們在類中聲明的次序(與參數表順序無關)
3. 調用派生類構造函數
#include<iostream>
using namespace std;

class Base1
{
    public:
        Base1(int x)
            {cout<<"基類1構造函數"<<"X1= "<<x<<endl;}
        ~Base1()
            {cout<<"基類1析構函數"<<endl; }
};
class Base2
{
    public:
        Base2(int x)
            {cout<<"基類2構造函數"<<"X2= "<<x<<endl;}
        ~Base2()
            {cout<<"基類2析構函數"<<endl; }
};
class Base3
{
    public:
        Base3()
            {cout<<"基類3構造函數"<<endl;}
        ~Base3()
            {cout<<"基類3析構函數"<<endl; }
};

class A:public Base2,public Base3,public Base1
{
    public:
        A(int a,int b,int c,int d)
            :Base1(a),Base2(b),m1(c),m3(),m2(d)
            //此處如果基類構造函數沒有參數,則可省略
            //基類和子函數的陳列,且順序隨意 
            //or:Base1(a),Base2(b),m1(c),m2(d)
        {
            cout<<"派生類構造函數"<<endl;
        }
        ~A()
        {
            cout<<"派生類析構函數"<<endl;
        }
    private:
    Base1 m1;
    Base2 m2;
    Base3 m3; 
};

int main()
{
    A obj(1,2,3,4);
    return 0;
}

//Output
基類2構造函數X2=2
基類3構造函數			//若基類無帶參構造函數可以省略,因此參數表中無基類3
基類1構造函數X1=1
基類1構造函數X1=3		//我們需要按照他們在類中聲明的順序來分別構造基類1、基類2、基類3
基類2構造函數X2=4
基類3構造函數
派生類構造函數
派生類析構函數
基類3析構函數
基類2析構函數
基類1析構函數
基類1析構函數
基類3析構函數
基類2析構函數
虛基類
  • 虛基類的構造函數在非虛基類之前調用
  • 若同一層次中包含多個虛基類,這些虛基類的構造函數按它們說明的順序調用
  • 若虛基類由非虛基類派生而來,則仍然遵循先調用基類構造函數,再調用派生類中構造函數的執行順序
class base1
{
public:
	base1() { cout << "class base1" << endl; }
};

class base2
{
public:
	base2() {cout << "class base2" << endl; }
}

class level1:public base2, virtual public base1
{
public:
	level1() {cout <<"class level1" << endl;}
}

class level2:public base2, virtual public base1
{
public:
	level2() {cout <<"class level1" << endl;}
}

class toplevel:public level1, virtual public level2
{
public:
	toplevel() {cout <<"class toplevel" << endl;}
}

void main() { toplevel obj; }

//output
class base1
class base2
class level2
class base2
class level1
class toplevel1

toplevel1類中,level2為虛基類,因此,盡管level1在level2之前說明,但還是level2的執行在先。
level2中,base1類為虛基類,先執行。同一個虛基類只需要初始化一次,所以level1時不需要再初始化base1
Alt text

Chapter-6 繼承與派生(錯題)


  • [例7.31]
class base
{
public:
	void who() { cout<<"base class"<<endl; }
}
class derive1:public base
{
public:
	void who() { cout<<"derive1 class"<<endl; }
}
class derive2:public base
{
public:
	void who() { cout<<"derive2 class"<<endl; }
}

void main()
{
	base obj1,*p;
	derive1 obj2;
	derivel obj3;
	p = &obj1;
	p->who();
	p = &obj2;
	p->who();
	p = &obj3;
	p->who();
	obj2.who();
	obj3.who();
}

//output:
base class
base class
base class
derive1 class
derive2 class

指針引起的普通成員函數的調用僅僅與指針類型有關,和此刻指針正指向的對象無關

Chapter-7 多態性和虛函數


靜態聯編&動態聯編
  • 靜態聯編:編譯時就解決了程序中的操作調用與執行該代碼間的關系
  • 動態聯編:只有在程序執行時才能確定將要調用的函數(虛函數支持下實現)
    • 動態聯編實現條件:
        1. 類之間為基類與派生類關系
        1. 要有虛函數
        1. 調用虛函數操作的是指向對象的指針或者對象引用,或者由成員函數調用虛函數
虛函數
  • 一個函數被聲明為虛函數,即使重新定義類時沒有聲明虛函數,那么它從這點之后的繼承層次結構中都是虛函數(若派生類聲明為虛函數,基類沒有聲明為虛函數,應該以基類為准)
  • 派生類的虛函數與基類中對應的虛函數的參數不同時,派生類的虛函數將丟失虛特性,變為重載函數
純虛函數 & 抽象類

純虛函數:基類中不能對虛函數給出有意義的定義
抽象類:帶有純虛函數的類稱為抽象類,唯一用途是為其他類提供合適的基類

class Class_Name
{
	virtual 類型 函數名(參數名) = 0;
}
虛析構函數
class A
{
	virtual ~A() { cout<<"調用A::~A()" << endl; };
}

class B:public A
{
	virtual ~B() { cout<<"調用B::~B()" << endl; };
}

void main()
{
	A *a = new B();
}

//Output
調用B::~B()
調用A::~A()

//如果沒有虛析構函數Output
調用A::~A()

Chapter-8 異常處理


異常基礎概念

catch處理程序的出現順序很重要,在一個try塊中,異常處理程序是按照它出現的順序被檢查的。只要找到一個匹配的異常類型,后面的異常處理都將被忽略

void f(int code)
{
	try
	{
		if(code==0) throw code;     //引發int類型的yi'chang
		if(code==1) throw 'x';      //引發字符型異常
		if(code==2) throw 12.345;   //引發double類型異常
	}
	catch(int n)
	{ cout << "捕獲整數類型: "<< n << endl;}
	catch(...)                      //可以捕獲任何異常
 	{ cout << "默認捕獲"<< endl;}
	return;
}
void main()
{
	f(0);
	f(1);
	f(2);	
}

//Output
捕獲整數類型: 0
默認捕獲
默認捕獲


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM