日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > c/c++ >内容正文

c/c++

史上最全的C++面试宝典(合集)

發布時間:2024/8/1 c/c++ 37 豆豆
生活随笔 收集整理的這篇文章主要介紹了 史上最全的C++面试宝典(合集) 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

參考:https://www.runoob.com/cplusplus/cpp-tutorial.html

本教程旨在提取最精煉、實用的C++面試知識點,供讀者快速學習及本人查閱復習所用。

目錄

第一章? C++基本語法

1.1? C++程序結構

1.2? 命名空間

1.3? 預處理器

1.4? 相關面試題

第二章? C++數據操作

2.1? 數據類型

2.2? 變量

2.3? 常量

2.4? 類型限定符

2.5? 存儲類

2.6? 運算符

2.7? 相關面試題

第三章? 指針和引用

3.1? 指針

3.2? 引用

3.3? 相關面試題

第四章? 函數——C++的編程模塊

4.1? 函數的定義與聲明

4.2? 內聯函數

4.3? 重載

4.4? 模板

4.5? 相關面試題

第五章? 結構體、類與對象

5.1? 結構體

5.2? 類和對象

5.3? 數據抽象與封裝

5.4? 繼承

5.5? 多態

5.6? 相關面試題

第六章? 動態內存

6.1? new和delete運算符

6.2? 動態內存分配

6.3? 相關面試題

第七章? C++ STL(標準模板庫)

7.1? 容器

7.2? 相關面試題

第八章? 異常處理

8.1? 拋出異常

8.2? 捕獲異常

8.3? C++標準的異常

8.4? 定義新的異常

第九章? 多線程

9.1? 基本概念

9.2? C++線程管理

9.3? 線程的同步與互斥

9.4? C++中的幾種鎖

9.5? C++中的原子操作

9.6??相關面試題


第一章? C++基本語法

C++ 程序可以定義為對象的集合,這些對象通過調用彼此的方法進行交互。

  • 對象 -?對象具有狀態和行為。例如:一只狗的狀態 - 顏色、名稱、品種,行為 - 搖動、叫喚、吃。
  • 類 -?類可以定義為描述對象行為/狀態的模板,對象是類的實例。
  • 方法 -?從基本上說,一個方法表示一種行為。一個類可以包含多個方法??梢栽诜椒ㄖ袑懭脒壿嫛⒉僮鲾祿葎幼?。
  • 即時變量 -?每個對象都有其獨特的即時變量。

1.1? C++程序結構

下面給出一段基礎的C++程序:

#include <iostream> using namespace std; // main() 是程序開始執行的地方 int main() {cout << "Hello World" << endl; // 輸出 Hello Worldreturn 0; }

這段程序主要結構如下:

  • C++ 語言定義了一些頭文件,這些頭文件包含了程序中必需的或有用的信息。上面這段程序中,包含了頭文件 <iostream>
  • using namespace std; 告訴編譯器使用 std 命名空間。
  • int main() 是主函數,程序從這里開始執行。

1.2? 命名空間

  • 命名空間這個概念可作為附加信息來區分不同庫中相同名稱的函數、類、變量等。
  • 使用了命名空間即定義了上下文,本質上,命名空間就是定義了一個范圍。

1.2.1? 定義命名空間

下面通過一個示例來展示如何定義命名空間并使用命名空間中的函數等。

#include <iostream> using namespace std;// 第一個命名空間 namespace first_space{void func(){cout << "Inside first_space" << endl;} } // 第二個命名空間 namespace second_space{void func(){cout << "Inside second_space" << endl;} } int main () {// 調用第一個命名空間中的函數first_space::func();// 調用第二個命名空間中的函數second_space::func(); return 0; }

1.2.2? using指令

可以使用 using namespace xxxx指令,這樣在使用命名空間時就可以不用在前面加上命名空間的名稱。這個指令會告訴編譯器,后續的代碼將使用指定的命名空間中的名稱。

1.3? 預處理器

預處理器是一些指令,指示編譯器在實際編譯之前所需完成的預處理。所有的預處理器指令都是以井號(#)開頭,只有空格字符可以出現在預處理指令之前。預處理指令不是 C++ 語句,所以它們不會以分號(;)結尾。

1.3.1? #define預處理

#define 預處理指令用于創建符號常量。該符號常量通常稱為宏,指令的一般形式是:

#define macro-name replacement-text //例如 #define PI 3.14159

可以使用 #define 來定義一個帶有參數的參數宏,如下所示:

#include <iostream> using namespace std;#define MIN(a,b) (a<b ? a : b)int main () {int i, j;i = 100;j = 30;cout <<"較小的值為:" << MIN(i, j) << endl;return 0; }

1.3.2? 條件編譯

有幾個指令可以用來有選擇地對部分程序源代碼進行編譯。這個過程被稱為條件編譯。

條件預處理器的結構與 if 選擇結構很像。請看下面這段預處理器的代碼:

#ifndef NULL#define NULL 0 #endif

例如,要實現只在調試時進行編譯,可以使用一個宏來實現,如下所示:

#ifdef DEBUGcerr <<"Variable x = " << x << endl; #endif

使用 #if 0 語句可以注釋掉程序的一部分,如下所示:

#if 0不進行編譯的代碼 #endif

下面給出一個示例:

#include <iostream> using namespace std; #define DEBUG#define MIN(a,b) (((a)<(b)) ? a : b)int main () {int i, j;i = 100;j = 30; #ifdef DEBUGcerr <<"Trace: Inside main function" << endl; #endif#if 0/* 這是注釋部分 */cout << MKSTR(HELLO C++) << endl; #endifcout <<"The minimum is " << MIN(i, j) << endl;#ifdef DEBUGcerr <<"Trace: Coming out of main function" << endl; #endifreturn 0; }

當上面的代碼被編譯和執行時,它會產生下列結果:

Trace: Inside main function
The minimum is 30
Trace: Coming out of main function

1.4? 相關面試題

Q:C++和C的區別

A:設計思想上:

  • C++是面向對象的語言,而C是面向過程的結構化編程語言

語法上:

  • C++具有封裝、繼承和多態三種特性
  • C++相比C,增加多許多類型安全的功能,比如強制類型轉換
  • C++支持范式編程,比如模板類、函數模板等

Q:為什么C++支持函數重載而C語言不支持呢

A:在鏈接階段,C語言是通過函數本名去尋找函數的實體的,所以當兩個函數同名時是無法識別的;而C++會將函數名和函數帶的參數轉換成編譯特征和固有特征,這時候編譯器就可以分辨出兩個同名不同參的函數。

Q:include頭文件雙引號””和尖括號<>的區別

A:編譯器預處理階段查找頭文件的路徑不一樣:

  • 對于使用雙引號包含的頭文件,編譯器從用戶的工作路徑開始搜索
  • 對于使用尖括號包含的頭文件,編譯器從標準庫路徑開始搜索

Q:頭文件的作用是什么?

A:

  • 通過頭文件來調用庫功能。
  • 頭文件能加強類型安全檢查。
  • Q:在頭文件中進行類的聲明,在對應的實現文件中進行類的定義有什么意義?

    A:這樣可以提高編譯效率,因為分開的話,這個類只需要編譯一次生成對應的目標文件,以后在其他地方用到這個類時,編譯器查找到了頭文件和目標文件,就不會再次編譯這個類,從而大大提高了效率。

    Q:C++源文件從文本到可執行文件經歷的過程

    A:對于C++源文件,從文本到可執行文件一般需要四個過程:

  • 預編譯階段:對源代碼文件中文件包含關系(頭文件)、預編譯語句(宏定義)進行分析和替換,生成預編譯文件
  • 編譯階段:將經過預處理后的預編譯文件轉換成特定匯編代碼,生成匯編文件
  • 匯編階段:將編譯階段生成的匯編文件轉化成機器碼,生成可重定位目標文件
  • 鏈接階段:將多個目標文件及所需要的庫鏈接成最終的可執行目標文件
  • Q:靜態鏈接與動態鏈接

    A:靜態鏈接是在編譯期間完成的。

    • 靜態鏈接浪費空間 ,這是由于多進程情況下,每個進程都要保存靜態鏈接函數的副本。
    • 更新困難 ,當鏈接的眾多目標文件中有一個改變后,整個程序都要重新鏈接才能使用新的版本。
    • 靜態鏈接運行效率高。

    動態鏈接的進行則是在程序執行時鏈接。

    • ?動態鏈接當系統多次使用同一個目標文件時,只需要加載一次即可,節省內存空間。
    • 程序升級變得容易,當升級某個共享模塊時,只需要簡單的將舊目標文件替換掉,程序下次運行時,新版目標文件會被自動裝載到內存并鏈接起來,即完成升級。

    Q:C++11有哪些新特性

    A:

    • auto關鍵字:編譯器可以根據初始值自動推導出類型,但是不能用于函數傳參以及數組類型的推導;
    • nullptr關鍵字:nullptr是一種特殊類型的字面值,它可以被轉換成任意其它的指針類型;而NULL一般被宏定義為0,在遇到重載時可能會出現問題。
    • 智能指針:C++11新增了std::shared_ptr、std::weak_ptr等類型的智能指針,用于解決內存管理的問題。
    • 初始化列表:使用初始化列表來對類進行初始化
    • 右值引用:基于右值引用可以實現移動語義和完美轉發,消除兩個對象交互時不必要的對象拷貝,節省運算存儲資源,提高效率
    • atomic原子操作用于多線程資源互斥操作
    • 新增STL容器array以及tuple

    Q:assert()是什么

    A:斷言是宏,而非函數。assert 宏的原型定義在 <assert.h>(C)、<cassert>(C++)中,其作用是如果它的條件返回錯誤,則終止程序執行。可以通過定義 NDEBUG 來關閉 assert,但是需要在源代碼的開頭,include <assert.h> 之前。

    #define NDEBUG // 加上這行,則 assert 不可用 #include <assert.h> assert( p != NULL ); // assert 不可用

    Q:C++是不是類型安全的?

    A:不是,因為兩個不同類型的指針之間可以強制轉換(用reinterpret cast)。

    Q:系統會自動打開和關閉的3個標準的文件是?

    A:

  • 標準輸入----鍵盤---stdin
  • 標準輸出----顯示器---stdout
  • 標準出錯輸出----顯示器---stder
  • ?

    第二章? C++數據操作

    2.1? 數據類型

    2.1.1? 基本類型

    C++有7種基本的數據類型:

    基本數據類型

    可以使用signed,unsigned,short,long去修飾這些基本類型:

    類型及大小

    2.1.2? typedef

    可以使用 typedef 為一個已有的類型取一個新的名字。例如:

    //typedef type newname; typedef int feet; feet distancetypedef struct Student {int age; } S; S student;

    2.2? 變量

    2.2.1? 變量定義

    //type variable_name = value; extern int d = 3, f = 5; // d 和 f 的聲明 int d = 3, f = 5; // 定義并初始化 d 和 f byte z = 22; // 定義并初始化 z char x = 'x'; // 變量 x 的值為 'x'

    2.2.2? 變量聲明
    可以使用extern關鍵字在任意地方聲明一個變量。

    // 變量聲明 extern int a, b; extern float f;int main () {// 變量定義int a, b;float f;return 0; }

    同樣的,函數聲明是,提供一個函數名即可,而函數的實際定義則可以在任何地方進行。

    // 函數聲明 int func();int main() {// 函數調用int i = func(); }// 函數定義 int func() {return 0; }

    2.2.3? 變量作用域

  • 在函數或一個代碼塊內部聲明的變量,稱為局部變量。
  • 在函數參數的定義中聲明的變量,稱為形式參數。
  • 在所有函數外部聲明的變量,稱為全局變量。
  • 注:當局部變量被定義時,系統不會對其初始化,您必須自行對其初始化。定義全局變量時,系統會自動初始化為下列值:

    變量初始化

    2.3? 常量

    常量是固定值,在程序執行期間不會改變。這些固定的值,又叫做字面量。

    2.3.1? define預處理器

    下面是使用 #define 預處理器定義常量的形式:

    #define LENGTH 10 #define WIDTH 5 #define NEWLINE '\n'

    2.3.2? const關鍵字

    可以使用 const 前綴聲明指定類型的常量,const類型的對象在程序執行期間不能被修改。如下所示:

    const int LENGTH = 10; const int WIDTH = 5; const char NEWLINE = '\n';

    2.4? 類型限定符

    2.4.1? volatile

    volatile?用來修飾變量,表明某個變量的值可能會隨時被外部改變,因此使用 volatile 告訴編譯器不應對這樣的對象進行優化(沒有被 volatile 修飾的變量,可能由于編譯器的優化,從 CPU 寄存器中取值;而被volatile修飾的變量,它不能被緩存到寄存器,每次訪問需要到內存中重新讀取)。

    volatile int n = 10;

    2.4.2? restrict

    由 restrict 修飾的指針是唯一一種訪問它所指向的對象的方式。

    2.5? 存儲類

    存儲類定義 C++ 程序中變量/函數的范圍(可見性)和生命周期。

    2.5.1? auto存儲類

    auto?關鍵字用于兩種情況:聲明變量時根據初始化表達式自動推斷該變量的類型、聲明函數時函數返回值的占位符。

    2.5.2??static存儲類

    static?存儲類指示編譯器在程序的生命周期內保持局部變量的存在,而不需要在每次它進入和離開作用域時進行創建和銷毀。

    • 使用 static 修飾局部變量可以在函數調用之間保持局部變量的值。
    • 當 static 修飾全局變量時,會使變量的作用域限制在聲明它的文件內。
    • 在 C++ 中,當 static 用在類數據成員上時,會導致僅有一個該成員的副本被類的所有對象共享。

    2.5.3??extern存儲類

    extern?存儲類用于提供一個全局變量的引用,全局變量對所有的程序文件都是可見的。通常用于當有兩個或多個文件共享相同的全局變量或函數的時候。

    2.6? 運算符

    算數運算符???

    ?

    ?

    關系運算符

    ?

    邏輯運算符

    ?

    位運算符

    ?

    賦值運算符

    ?

    雜項運算符

    ?

    運算符優先級

    2.7? 相關面試題

    Q:const的作用

    A:

  • 修飾變量,說明該變量不可以被修改
  • 修飾指針,即常量指針和指針常量
  • 常量引用,經常用于形參類型,既避免了拷貝,又避免了函數對值的修改
  • 修飾類的成員函數,說明該成員函數內不能修改成員變量
  • // 類 class A { private:const int a; // 常對象成員,只能在初始化列表賦值 public:// 構造函數A() : a(0) { };A(int x) : a(x) { }; // 初始化列表// const可用于對重載函數的區分int getValue(); // 普通成員函數int getValue() const; // 常成員函數,不得修改類中的任何數據成員的值 };void function() {// 對象A b; // 普通對象,可以調用全部成員函數、更新常成員變量const A a; // 常對象,只能調用常成員函數const A *p = &a; // 常指針const A &q = a; // 常引用// 指針char greeting[] = "Hello";char* p1 = greeting; // 指針變量,指向字符數組變量const char* p2 = greeting; // 常量指針即常指針,指針指向的地址可以改變,但是所存的內容不能變char const* p2 = greeting; // 與const char* p2 等價char* const p3 = greeting; // 指針常量,指針是一個常量,即指針指向的地址不能改變,但是指針所存的內容可以改變const char* const p4 = greeting; // 指向常量的常指針,指針和指針所存的內容都不能改變,本質是一個常量 }// 函數 void function1(const int Var); // 傳遞過來的參數在函數內不可變 void function2(const char* Var); // 參數為常量指針即指針所指的內容為常量不能變,指針指向的地址可以改變 void function3(char* const Var); // 參數為指針常量 void function4(const int& Var); // 參數為常量引用,在函數內部不會被進行修改,同時參數不會被復制一遍,提高了效率// 函數返回值 const int function5(); // 返回一個常數 const int* function6(); // 返回一個指向常量的指針變量即常量指針,使用:const int *p = function6(); int* const function7(); // 返回一個指向變量的常指針即指針常量,使用:int* const p = function7();

    Q:說明define和const在語法和含義上有什么不同?

    A:

  • #define是C語法中定義符號變量的方法,符號常量只是用來表達一個值,在編譯階段符號就被值替換了,它沒有類型;
  • const是C++語法中定義常變量的方法,常變量具有變量特性,它具有類型,內存中存在以它命名的存儲單元,可以用sizeof測出長度。
  • Q:static關鍵字的作用

    A:靜態變量在程序執行之前就創建,在程序執行的整個周期都存在??梢詺w類為如下五種:

  • 局部靜態變量:作用域僅在定義它的函數體或語句塊內,該變量的內存只被分配一次,因此其值在下次函數被調用時仍維持上次的值;
  • 全局靜態變量:作用域僅在定義它的文件內,該變量也被分配在靜態存儲區內,在整個程序運行期間一直存在;
  • 靜態函數:在函數返回類型前加static,函數就定義為靜態函數。靜態函數只是在聲明他的文件當中可見,不能被其他文件所用;
  • 類的靜態成員:在類中,靜態成員屬于整個類所擁有,對類的所有對象只有一份拷貝,因此可以實現多個對象之間的數據共享,并且使用靜態數據成員還不會破壞隱藏的原則,即保證了安全性;
  • 類的靜態函數:在類中,靜態成員函數不接收this指針,因而只能訪問類的static成員變量,如果靜態成員函數中要引用非靜態成員時,可通過對象來引用。(調用靜態成員函數使用如下格式:<類名>::<靜態成員函數名>(<參數表>);)
  • Q:請你來說一下C++里是怎么定義常量的?常量存放在內存的哪個位置?

    A:常量在C++里使用const關鍵字定義,常量定義必須初始化。對于局部對象,常量存放在棧區;對于全局對象,編譯期一般不分配內存,放在符號表中以提高訪問效率;對于字面值常量,存放在常量存儲區。

    Q:sizeof()和strlen()

    A:sizeof是運算符,能獲得保證能容納實現所建立的最大對象的字節大小:

    • sizeof 對數組,得到整個數組所占空間大小;
    • sizeof 對指針,得到指針本身所占空間大小(4個字節);
    • 當一個類A中沒有生命任何成員變量與成員函數,這時sizeof(A)的值是1。

    strlen()是函數,可以計算字符串的長度,直到遇到結束符NULL才結束,返回的長度大小不包含NULL。

    Q:C++ 內存對齊

    A:

    1)內存對齊的定義

    數據項只能存儲在地址是數據項大小的整數倍的內存位置上。現代計算機中內存空間都是按照byte劃分的,從理論上講似乎對任何類型的變量的訪問可以從任何地址開始,但實際情況是在訪問特定類型變量的時候經常在特定的內存地址訪問,這就需要各種類型數據按照一定的規則在空間上排列,而不是順序的一個接一個的排放,這就是對齊。

    2)使用原因

  • 平臺原因:不同硬件平臺對存儲空間的處理上存在很大的不同。某些平臺對特定類型的數據只能從特定地址開始存取,而不允許其在內存中任意存放;
  • 性能原因:為了訪問未對齊的內存,處理器需要作兩次內存訪問,而對齊的內存訪問僅需要一次訪問;如果不按照平臺要求對存放數據進行對齊,會發生內存的二次訪問,帶來存取效率上的損失。
  • 3)內存對齊的規則

  • 數據成員對齊規則:結構(struct)(或聯合(union))的數據成員,第一個數據成員放在offset為0的地方,以后每個數據成員的對齊按照#pragma pack指定的數值和這個數據成員自身長度中,比較小的那個進行;
  • 結構(或聯合)的整體對齊規則:在數據成員完成各自對齊之后,結構(或聯合)本身也要進行對齊,對齊將按照#pragma pack指定的數值和結構(或聯合)最大數據成員長度中,比較小的那個進行;
  • 結構體作為成員:如果一個結構里有某些結構體成員,則結構體成員要從其內部最大元素大小的整數倍地址開始存儲。
  • 4)位域

    相鄰的多個同類型的數(帶符號的與不帶符號的,只要基本類型相同,也為相同的數),如果他們占用的位數不超過基本類型的大小,那么他們可作為一個整體來看待。不同類型的數要遵循各自的對齊方式。

    struct AA {unsigned int a : 10;unsigned char b : 4;short c; };struct BB {unsigned int a : 10;unsigned int b : 4;short c; };

    因此,結構體AA的大小為8字節(a占4字節,b占2字節,c占2字節),BB為8字節(a、b占4字節,c占2字節并補齊)。

    Q:強制類型轉換運算符

    A:static_cast

    • 特點:靜態轉換,編譯時執行。
    • 應用場合:主要用于C++中內置的基本數據類型之間的轉換,同一個繼承體系中類型的轉換,任意類型與空指針類型void* 之間的轉換,但是沒有運行時類型檢查(RTTI)來保證轉換的安全性。

    const_cast

    • 特點:去常轉換,編譯時執行。
    • 應用場合:?const_cast可以用于修改類型的const或volatile屬性,去除指向常數對象的指針或引用的常量性。

    reinterpret_cast:

    • 特點:重解釋類型轉換,編譯時執行。
    • 應用場合: 可以用于任意類型的指針之間的轉換,對轉換的結果不做任何保證。

    dynamic_cast:

    • 特點:動態類型轉換,運行時執行。
    • 應用場合:只能用于存在虛函數的父子關系的強制類型轉換,只能轉指針或引用。對于指針,轉換失敗則返回nullptr,對于引用,轉換失敗會拋出異常

    Q:請你說說你了解的RTTI

    A:

    定義:RTTI(Run Time Type Identification)即通過運行時類型識別,程序能夠使用基類的指針或引用來檢查著這些指針或引用所指的對象的實際派生類型。

    RTTI機制產生原因:C++是一種靜態類型語言,其數據類型是在編譯期就確定的,不能在運行時更改。然而由于面向對象程序設計中多態性的要求,C++中的指針或引用本身的類型,可能與它實際代表(指向或引用)的類型并不一致。有時我們需要將一個多態指針轉換為其實際指向對象的類型,就需要知道運行時的類型信息,這就產生了運行時類型識別的要求。

    C++中有兩個函數用于運行時類型識別,分別是dynamic_cast和typeid,具體如下:

    • typeid函數返回一個對type_info類對象的引用,可以通過該類的成員函數獲得指針和引用所指的實際類型;
    • dynamic_cast操作符,將基類類型的指針或引用安全地轉換為其派生類類型的指針或引用。

    Q:explicit(顯式)關鍵字

    A:

    • explicit 修飾構造函數時,可以防止隱式轉換和復制初始化,必須顯式初始化
    • explicit 修飾轉換函數時,可以防止隱式轉換,但按語境轉換 除外
    struct B {explicit B(int) {}explicit operator bool() const { return true; } };int main() {B b1(1); // OK:直接初始化B b2 = 1; // 錯誤:被 explicit 修飾構造函數的對象不可以復制初始化B b3{ 1 }; // OK:直接列表初始化B b4 = { 1 }; // 錯誤:被 explicit 修飾構造函數的對象不可以復制列表初始化B b5 = (B)1; // OK:允許 static_cast 的顯式轉換doB(1); // 錯誤:被 explicit 修飾構造函數的對象不可以從 int 到 B 的隱式轉換if (b1); // OK:被 explicit 修飾轉換函數 B::operator bool() 的對象可以從 B 到 bool 的按語境轉換bool b6(b1); // OK:被 explicit 修飾轉換函數 B::operator bool() 的對象可以從 B 到 bool 的按語境轉換bool b7 = b1; // 錯誤:被 explicit 修飾轉換函數 B::operator bool() 的對象不可以隱式轉換bool b8 = static_cast<bool>(b1); // OK:static_cast 進行直接初始化return 0; }

    Q::: 范圍解析運算符

    A:該運算符可分為如下三類:

    • 全局作用域符(::name):用于類型名稱(類、類成員、成員函數、變量等)前,表示作用域為全局命名空間
    • 類作用域符(class::name):用于表示指定類型的作用域范圍是具體某個類的
    • 命名空間作用域符(namespace::name):用于表示指定類型的作用域范圍是具體某個命名空間的

    ?

    第三章? 指針和引用

    3.1? 指針

    3.1.1? 指針定義

    指針是一個變量,其值為另一個變量的內存地址。指針變量聲明的一般形式為:

    type *var-name; int *ip; /* 一個整型的指針 */

    所有指針的值的實際數據類型,不管是整型、浮點型、字符型,還是其他的數據類型,都是一樣的,都是一個代表內存地址的長的十六進制數。不同數據類型的指針之間唯一的不同是,指針所指向的變量或常量的數據類型不同。

    3.1.2? 指針的使用

    使用指針時會頻繁進行以下幾個操作:定義一個指針變量、把變量地址賦值給指針、訪問指針變量中可用地址的值。這些是通過使用一元運算符 * 來返回位于操作數所指定地址的變量的值。

    #include <iostream>using namespace std;int main () {int var = 20; // 實際變量的聲明int *ip; // 指針變量的聲明ip = &var; // 在指針變量中存儲 var 的地址cout << "Value of var variable: ";cout << var << endl;// 輸出在指針變量中存儲的地址cout << "Address stored in ip variable: ";cout << ip << endl;// 訪問指針中地址的值cout << "Value of *ip variable: ";cout << *ip << endl;return 0; }

    其結果為:

    Value of var variable: 20
    Address stored in ip variable: 0xbfc601ac
    Value of *ip variable: 20

    3.1.3? 常用指針操作

    //空指針 int *ptr = NULL; cout << "ptr 的值是 " << ptr ; //結果是:ptr 的值是 0//指針遞增 int var[3] = {10, 100, 200}; ptr = var; //數組的變量名代表指向第一個元素的指針 ptr++;//指向指針的指針 int var; int *ptr; int **pptr;var = 3000; // 獲取 var 的地址 ptr = &var; // 使用運算符 & 獲取 ptr 的地址 pptr = &ptr;

    3.2? 引用

    引用變量是一個別名,也就是說,它是某個已存在變量的另一個名字。一旦把引用初始化為某個變量,就可以使用該引用名稱或變量名稱來指向變量。引用很容易與指針混淆,它們之間有三個主要的不同:

  • 不存在空引用。引用必須連接到一塊合法的內存。
  • 一旦引用被初始化為一個對象,就不能被指向到另一個對象。指針可以在任何時候指向到另一個對象。
  • 引用必須在創建時被初始化。指針可以在任何時間被初始化。
  • // 聲明簡單的變量 int i; double d;// 聲明引用變量 int& r = i; double& s = d;

    3.3? 相關面試題

    Q:C/C++ 中指針和引用的區別?

    A:

  • 指針有自己的一塊空間,而引用只是一個別名;
  • 指針可以被初始化為NULL,而引用必須被初始化;
  • 指針在使用中可以指向其它對象,但是引用只能是一個對象的引用,不能被改變;
  • 指針可以有多級指針(**p),而引用只有一級;
  • Q:指針函數和函數指針?

    A:

    • 指針函數本質上是一個函數,函數的返回值是一個指針;
    • 函數指針本質上是一個指針,C++在編譯時,每一個函數都有一個入口地址,該入口地址就是函數指針所指向的地址,有了函數指針后,就可用該指針變量調用函數。
    char * fun(char * p) {…} // 指針函數fun char * (*pf)(char * p); // 函數指針pf pf = fun; // 函數指針pf指向函數fun pf(p); // 通過函數指針pf調用函數fun

    Q:在什么時候需要使用“常引用”?

    A:如果既要利用引用提高程序的效率,又要保護傳遞給函數的數據不在函數中被改變,就應使用常引用。

    Q:C++中的四個智能指針: shared_ptr、unique_ptr、weak_ptr、auto_ptr

    A:智能指針出現的原因:智能指針的作用就是用來管理一個指針,將普通的指針封裝成一個棧對象,當棧對象的生命周期結束之后,會自動調用析構函數釋放掉申請的內存空間,從而防止內存泄露。(https://www.cnblogs.com/WindSun/p/11444429.html)

  • shared_ptr實現共享式擁有概念。多個智能指針指向相同對象,該對象和其相關資源會在最后一個引用被銷毀時被釋放。
  • unique_ptr實現獨占式擁有概念,保證同一時間內只有一個智能指針可以指向該對象。
  • weak_ptr 是一種共享但不擁有對象的智能指針, 它指向一個 shared_ptr 管理的對象。進行該對象的內存管理的是那個強引用的 shared_ptr,weak_ptr只是提供了對管理對象的一個訪問手段,它的構造和析構不會引起引用計數的增加或減少。weak_ptr 設計的目的是為協助?shared_ptr工作的,用來解決shared_ptr相互引用時的死鎖問題。注意的是我們不能通過weak_ptr直接訪問對象的方法,可以通過調用lock函數來獲得shared_ptr,再通過shared_ptr去調用對象的方法。
  • auto_ptr采用所有權模式,C++11中已經拋棄。
  • Q:shared_ptr的底層實現

    A:

    template <typename T> class smart_ptrs { public:smart_ptrs(T*); //用普通指針初始化智能指針smart_ptrs(smart_ptrs&); // 拷貝構造T* operator->(); //自定義指針運算符T& operator*(); //自定義解引用運算符smart_ptrs& operator=(smart_ptrs&); //自定義賦值運算符~smart_ptrs(); //自定義析構函數 private:int *count; //引用計數T *p; //智能指針底層保管的指針 };//構造函數 template <typename T> smart_ptrs<T>::smart_ptrs(T *p): count(new int(1)), p(p) {}//對普通指針進行拷貝,同時引用計數器加1,因為需要對參數進行修改,所以沒有將參數聲明為const template <typename T> smart_ptrs<T>::smart_ptrs(smart_ptrs &sp): count(&(++*sp.count)), p(sp.p) {}//指針運算符 template <typename T> T* smart_ptrs<T>::operator->() {return p;}//定義解引用運算符 template <typename T> T& smart_ptrs<T>::operator*() {return *p;}//定義賦值運算符,左邊的指針計數減1,右邊指針計數加1,當左邊指針計數為0時,釋放內存: template <typename T> smart_ptrs<T>& smart_ptrs<T>::operator=(smart_ptrs& sp) {++*sp.count;if (--*count == 0) { //自我賦值同樣能保持正確delete count;delete p;}this->p = sp.p;this->count = sp.count;return *this; }// 定義析構函數: template <typename T> smart_ptrs<T>::~smart_ptrs() {if (--*count == 0) {delete count;delete p;} }

    Q:野指針

    A:野指針就是指向一個已銷毀或者訪問受限內存區域的指針。產生野指針通常是因為幾種疏忽:

  • 指針變量未被初始化;
  • 指針釋放后未置空;
  • 指針操作超越變量作用域(例如變量被釋放了,指針還是指向它)。
  • Q:什么時候會發生段錯誤?

    A:段錯誤通常發生在訪問非法內存地址的時候,具體來說分為以下幾種情況:

    • 使用了野指針
    • 試圖修改字符串常量的內容
    • 數組越界導致棧溢出

    Q:什么是右值引用,跟左值又有什么區別?

    A:左值:能對表達式取地址的具名對象/變量等。一般指表達式結束后依然存在的持久對象。

    右值:不能對表達式取地址的字面量、函數返回值、匿名函數或匿名對象。一般指表達式結束就不再存在的臨時對象。

    右值引用和左值引用的區別在于:

    • 通過&獲得左值引用,左值引用只能綁定左值。
    • 通過&&獲得右值引用,右值引用只能綁定右值,基于右值引用可以實現移動語義和完美轉發,右值引用的好處是減少右值作為參數傳遞時的復制開銷,提高效率。

    Q:什么是std::move()以及什么時候使用它?

    A:std::move()是C ++標準庫中用于轉換為右值引用的函數。當需要在其他地方“傳輸”對象的內容時使用std :: move,對象可以在不進行復制的情況下獲取臨時對象的內容,避免不必要的深拷貝。

    Q:C++類的內部可以定義引用數據成員嗎?

    A:可以,必須通過成員函數初始化列表初始化

    class MyClass { public:MyClass(int &i): a(1), b(i){ // 構造函數初始化列表中是初始化工作// 在這里做的是賦值而非初始化工作} private:const int a;int &b; // 引用數據成員b,必須通過列表初始化! };

    ?

    第四章? 函數——C++的編程模塊

    4.1? 函數的定義與聲明

    4.1.1 函數定義

    return_type function_name( parameter list ) {body of the function }// 示例:函數返回兩個數中較大的那個數 int max(int num1, int num2) {// 局部變量聲明int result;if (num1 > num2)result = num1;elseresult = num2;return result; }

    4.1.2? 函數聲明

    return_type function_name( parameter list ); //示例 int max(int num1, int num2); //在函數聲明中,參數的名稱并不重要,只有參數的類型是必需的,因此下面也是有效的聲明: int max(int, int);

    注:當你在一個源文件中定義函數且在另一個文件中調用函數時,函數聲明是必需的。在這種情況下,您應該在調用函數的文件頂部聲明函數。

    4.1.3? 函數參數

    如果函數要使用參數,則必須聲明接受參數值的變量。這些變量稱為函數的形式參數。形式參數就像函數內的其他局部變量,在進入函數時被創建,退出函數時被銷毀。當調用函數時,有多種向函數傳遞參數的方式:

    參數調用

    1、傳值調用

    默認情況下,C++ 使用傳值調用來傳遞參數。

    2、指針調用

    把參數的地址復制給形參

    #include <iostream> using namespace std;// 函數定義 void swap(int *x, int *y) {int temp;temp = *x; /* 保存地址 x 的值 */*x = *y; /* 把 y 賦值給 x */*y = temp; /* 把 x 賦值給 y */return; }int main () {// 局部變量聲明int a = 100;int b = 200;cout << "交換前,a 的值:" << a << endl;cout << "交換前,b 的值:" << b << endl;/* 調用函數來交換值* &a 表示指向 a 的指針,即變量 a 的地址 * &b 表示指向 b 的指針,即變量 b 的地址 */swap(&a, &b);cout << "交換后,a 的值:" << a << endl;cout << "交換后,b 的值:" << b << endl;return 0; }

    其中,&a、&b是指變量的地址,swap函數的形參*x、*y中的*是指從x、y的地址取值(即實參為地址,形參通過指針引用)。

    3、引用調用

    #include <iostream> using namespace std;// 函數定義 void swap(int &x, int &y) {int temp;temp = x; /* 保存地址 x 的值 */x = y; /* 把 y 賦值給 x */y = temp; /* 把 x 賦值給 y */return; }int main () {// 局部變量聲明int a = 100;int b = 200;cout << "交換前,a 的值:" << a << endl;cout << "交換前,b 的值:" << b << endl;/* 調用函數來交換值 */swap(a, b);cout << "交換后,a 的值:" << a << endl;cout << "交換后,b 的值:" << b << endl;return 0; }

    實參為變量,形參通過加&引用實參變量(區別于傳值引用)

    4、參數默認值

    int sum(int a, int b=20) {int result;result = a + b;return (result); }//調用的時候可以不傳入b sum(a);

    4.2? 內聯函數

    當函數被聲明為內聯函數之后,編譯器會將其內聯展開,而不是按通常的函數調用機制進行調用。引入內聯函數的目的是為了解決程序中函數調用的效率問題,程序在編譯器編譯的時候,編譯器將程序中出現的內聯函數的調用表達式用內聯函數的函數體進行替換,而對于其他的函數,都是在運行時候才被替代。這其實就是個空間代價換時間的節省。所以內聯函數一般都是10行以下的小函數,如果想把一個函數定義為內聯函數,則需要在函數名前面放置關鍵字 inline,在調用函數之前需要對函數進行定義。

    內聯函數注意點

    注:在類中定義的成員函數全部默認為內聯函數,在類中聲明,但在類外定義的為普通函數。

    4.3? 重載

    4.3.1? 重載函數

    C++ 允許在同一個作用域內聲明幾個功能類似的同名函數,但是這些同名函數的形式參數(指參數的個數、類型或者順序)必須不同。不能僅通過返回類型的不同來重載函數。

    調用一個重載函數或重載運算符時,編譯器通過把您所使用的參數類型與定義中的參數類型進行比較,決定選用最合適的定義。選擇最合適的重載函數或重載運算符的過程,稱為重載決策。

    #include <iostream> using namespace std;class printData {public:void print(int i) {cout << "整數為: " << i << endl;}void print(double f) {cout << "浮點數為: " << f << endl;}void print(char c[]) {cout << "字符串為: " << c << endl;} };int main(void) {printData pd;// 輸出整數pd.print(5);// 輸出浮點數pd.print(500.263);// 輸出字符串char c[] = "Hello C++";pd.print(c);return 0; }

    4.3.2? 重載運算符

    重載的運算符是帶有特殊名稱的函數,函數名是由關鍵字 operator 和其后要重載的運算符符號構成的。與其他函數一樣,重載運算符有一個返回類型和一個參數列表,例如:

    #include <iostream> using namespace std;class Box {public:double getVolume(void){return length * breadth * height;}void setLength( double len ){length = len;}void setBreadth( double bre ){breadth = bre;}void setHeight( double hei ){height = hei;}// 重載 + 運算符,用于把兩個 Box 對象相加Box operator+(const Box& b){Box box;box.length = this->length + b.length;box.breadth = this->breadth + b.breadth;box.height = this->height + b.height;return box;}private:double length; // 長度double breadth; // 寬度double height; // 高度 }; // 程序的主函數 int main( ) {Box Box1; // 聲明 Box1,類型為 BoxBox Box2; // 聲明 Box2,類型為 BoxBox Box3; // 聲明 Box3,類型為 Boxdouble volume = 0.0; // 把體積存儲在該變量中// Box1 詳述Box1.setLength(6.0); Box1.setBreadth(7.0); Box1.setHeight(5.0);// Box2 詳述Box2.setLength(12.0); Box2.setBreadth(13.0); Box2.setHeight(10.0);// Box1 的體積volume = Box1.getVolume();cout << "Volume of Box1 : " << volume <<endl;// Box2 的體積volume = Box2.getVolume();cout << "Volume of Box2 : " << volume <<endl;// 把兩個對象相加,得到 Box3Box3 = Box1 + Box2;// Box3 的體積volume = Box3.getVolume();cout << "Volume of Box3 : " << volume <<endl;return 0; }

    4.3.3? 可重載與不可重載運算符

    可重載運算符

    ?

    不可重載運算符

    4.4? 模板

    模板是泛型編程的基礎,泛型編程即以一種獨立于任何特定類型的方式編寫代碼。

    4.4.1? 函數模板

    模板函數定義的一般形式如下所示:

    template <class type> ret-type func-name(parameter list) {// 函數的主體 }

    實例如下:

    #include <iostream> #include <string>using namespace std;//使用const&可節省傳遞時間,同時保證值不被改變 template <typename T> inline T const& Max (T const& a, T const& b) { return a < b ? b:a; } int main () {int i = 39;int j = 20;cout << "Max(i, j): " << Max(i, j) << endl; double f1 = 13.5; double f2 = 20.7; cout << "Max(f1, f2): " << Max(f1, f2) << endl; string s1 = "Hello"; string s2 = "World"; cout << "Max(s1, s2): " << Max(s1, s2) << endl; return 0; }

    4.4.2? 類模板

    泛型類聲明的一般形式如下所示:

    template <class type> class class-name {//類的主體 }

    實例如下:

    #include <iostream> #include <vector> #include <cstdlib> #include <string> #include <stdexcept>using namespace std;template <class T> class Stack { private: vector<T> elems; // 元素 public: void push(T const&); // 入棧void pop(); // 出棧T top() const; // 返回棧頂元素bool empty() const{ // 如果為空則返回真。return elems.empty(); } }; template <class T> void Stack<T>::push (T const& elem) { // 追加傳入元素的副本elems.push_back(elem); } template <class T> void Stack<T>::pop () { if (elems.empty()) { throw out_of_range("Stack<>::pop(): empty stack"); }// 刪除最后一個元素elems.pop_back(); } template <class T> T Stack<T>::top () const { if (elems.empty()) { throw out_of_range("Stack<>::top(): empty stack"); }// 返回最后一個元素的副本 return elems.back(); } int main() { try { Stack<int> intStack; // int 類型的棧 Stack<string> stringStack; // string 類型的棧 // 操作 int 類型的棧 intStack.push(7); cout << intStack.top() <<endl; // 操作 string 類型的棧 stringStack.push("hello"); cout << stringStack.top() << std::endl; stringStack.pop(); stringStack.pop(); } catch (exception const& ex) { cerr << "Exception: " << ex.what() <<endl; return -1;} }

    4.5? 相關面試題

    Q:C語言是怎么進行函數調用的?

    A:每一個函數調用都會分配函數棧,在棧內進行函數執行過程。調用前,先把返回地址壓棧,然后把當前函數的esp指針壓棧。

    Q:函數參數壓棧方式為什么是從右到左的?

    A:因為C++支持可變函數參數。C程序棧底為高地址,棧頂為低地址,函數最左邊確定的參數在棧上的位置必須是確定的,否則意味著已經確定的參數是不能定位和找到的,這樣式無法保證函數正確執行的。

    Q:C++如何處理返回值

    A:生成一個臨時變量存入內存單元,調用程序訪問該內存單元,獲得返回值。

    Q:fork,wait,exec函數的作用

    A:

    • fork函數可以創建一個和當前映像一樣的子進程,這個函數會返回兩個值:從子進程返回0,從父進程返回子進程的PID;
    • 調用了wait函數的父進程會發生阻塞,直到有子進程狀態改變(執行成功返回0,錯誤返回-1);
    • exec函數可以讓子進程執行與父進程不同的程序,即讓子進程執行另一個程序(exec執行成功則子進程從新的程序開始運行,無返回值,執行失敗返回-1)。

    Q:inline內聯函數是什么?

    A:當一個函數被聲明為內聯函數之后,在編譯階段,編譯器會用內聯函數的函數體取替換程序中出現的內聯函數調用表達式,而其他的函數都是在運行時才被替換,這其實就是用空間換時間,提高了函數調用的效率。同時,內聯函數具有幾個特點:

  • 內聯函數中不可以出現循環、遞歸或開關操作;
  • 內聯函數的定義必須出現在內聯函數的第一次調用前;
  • 類的成員函數(除了虛函數)會自動隱式的當成內聯函數。
  • Q:內聯函數的優缺點

    A:優點:

  • 內聯函數在被調用處進行代碼展開,省去了參數壓棧、棧幀開辟與回收,結果返回等操作,從而提高程序運行速度;
  • 內聯函數相比宏函數來說,在代碼展開時,會做安全檢查或自動類型轉換,而宏定義則不會;
  • 在類中聲明同時定義的成員函數,自動轉化為內聯函數,因此內聯函數可以訪問類的成員變量,宏定義則不能;
  • 內聯函數在運行時可調試,而宏定義不可以。
  • 缺點:

  • 代碼膨脹,消耗了更多的內存空間;
  • inline 函數無法隨著函數庫升級而升級。inline函數的改變需要重新編譯,不像 non-inline 可以直接鏈接;
  • 內聯函數其實是不可控的,它只是對編譯器的建議,是否對函數內聯,決定權在于編譯器。
  • Q:虛函數可以是內聯函數嗎?

    A:虛函數可以是內聯函數,但是當虛函數表現多態性的時候不能內聯。

    Q:函數重載、重寫、隱藏和模板

    A:

    • 重載:在同一作用域中,兩個函數名相同,但是參數列表不同(個數、類型、順序),返回值類型沒有要求;
    • 重寫:子類繼承了父類,父類中的函數是虛函數,在子類中重新定義了這個虛函數,這種情況是重寫;
    • 隱藏:派生類中函數與基類中的函數同名,但是這個函數在基類中并沒有被定義為虛函數,此時基類的函數會被隱藏;
    • 模板:模板函數是一個通用函數,函數的類型和形參不直接指定而用虛擬類型來代表,只適用于參數個數相同類型不同的函數。

    Q:構造函數和析構函數能不能被重載?

    A:構造函數可以被重載,析構函數不可以被重載。因為構造函數可以有多個且可以帶參數, 而析構函數只能有一個,且不能帶參數。

    Q:拷貝構造函數和賦值運算符重載的區別?

    A:

  • 拷貝構造函數是函數,賦值運算符是運算符的重載;
  • 拷貝構造函數會生成新的類對象,賦值運算符不會;
  • 拷貝構造函數是用一個已存在的對象去構造一個不存在的對象;而賦值運算符重載函數是用一個存在的對象去給另一個已存在并初始化過的對象進行賦值。
  • Q:類模板是什么?

    A:類模板是對一批僅數據成員類型不同的類的抽象,用于解決多個功能相同、數據類型不同的類需要重復定義的問題。在建立類時候使用template及任意類型標識符T,之后在建立類對象時,會指定實際的類型,這樣才會是一個實際的對象。

    Q:select、poll和epoll的區別、原理、性能、限制

    A:select,poll,epoll都是I/O多路復用技術的具體實現。I/O多路復用就是在單個線程中,通過記錄并跟蹤每個I/O流的狀態,來同時管理多個I/O流,一旦某個I/O流已經就緒,就能夠通知程序進行相應的讀寫操作,以此提高服務器的吞吐能力。這種機制的優勢不是在于對單個連接能處理得更快,而是在于能處理更多的連接,也就是多路網絡連接復用一個I/O線程。

    select

    select是第一個實現I/O復用概念的函數。它用一個結構體fd_set讓內核監聽多個文件描述符。fd_set(文件描述符集合)本質上就是一個數組,當調用select函數后,就會去里面輪詢查找看是否有描述符被置位,也就是有需要被處理的I/O事件。

    select函數主要存在三個問題:

  • 內置數組的形式使得select支持的最大文件描述符受限于FD_SIZE;
  • 每次調用select前都要重新初始化描述符集,將fd從用戶態拷貝到內核態,每次調用select后,都需要將fd從內核態拷貝到用戶態;
  • 每次調用select后都要去輪詢排查所有文件描述符,這在文件描述符個數很多的時候,效率很低。
  • poll

    poll可以理解為一個加強版的select。它通過一個可變長度的數組解決了select文件描述符受限的問題。數組中元素是結構體pollfd,這個結構體保存了描述符的信息,每增加一個文件描述符就向數組中加入一個結構體。同時,結構體只需要拷貝一次到內核態,解決了select重復初始化的問題。但是,它仍然存在輪詢排查效率低的問題。

    epoll

    輪詢排查所有文件描述符的效率不高,使服務器并發能力受限。因此,epoll采用只返回狀態發生變化的文件描述符,便解決了輪尋的瓶頸。

    ?

    第五章? 結構體、類與對象

    5.1? 結構體

    5.1.1? 定義結構

    struct 語句定義了一個包含多個成員的新的數據類型,struct 語句的格式如下:

    struct Books {char title[50];char author[50];char subject[100];int book_id; } book;

    在結構定義的末尾,最后一個分號之前,可以指定一個或多個結構變量,這是可選的。上面是聲明一個結構體類型 Books,變量為 book。

    5.1.2? 訪問結構成員

    為了訪問結構的成員,我們使用成員訪問運算符(.)

    Books Book1; // 定義結構體類型 Books 的變量 Book1 // Book1 詳述 strcpy( Book1.title, "C++ 教程"); strcpy( Book1.author, "Runoob"); strcpy( Book1.subject, "編程語言"); Book1.book_id = 12345;

    5.1.3? 結構作為函數參數

    #include <iostream> #include <cstring>using namespace std; void printBook( struct Books book );// 聲明一個結構體類型 Books struct Books {char title[50];char author[50];char subject[100];int book_id; };int main( ) {Books Book1; // 定義結構體類型 Books 的變量 Book1Books Book2; // 定義結構體類型 Books 的變量 Book2// Book1 詳述strcpy( Book1.title, "C++ 教程");strcpy( Book1.author, "Runoob"); strcpy( Book1.subject, "編程語言");Book1.book_id = 12345;// Book2 詳述strcpy( Book2.title, "CSS 教程");strcpy( Book2.author, "Runoob");strcpy( Book2.subject, "前端技術");Book2.book_id = 12346;// 輸出 Book1 信息printBook( Book1 );// 輸出 Book2 信息printBook( Book2 );return 0; } void printBook( struct Books book ) {cout << "書標題 : " << book.title <<endl;cout << "書作者 : " << book.author <<endl;cout << "書類目 : " << book.subject <<endl;cout << "書 ID : " << book.book_id <<endl; }

    5.1.4? 指向結構的指針

    定義指向結構的指針,方式與定義指向其他類型變量的指針相似。為了使用指向該結構的指針訪問結構的成員,必須使用 -> 運算符,如下所示:

    struct Books *struct_pointer; struct_pointer = &Book1; struct_pointer->title;

    5.2? 類和對象

    5.2.1? 類的定義

    class Box {public:double length; // 盒子的長度double breadth; // 盒子的寬度double height; // 盒子的高度double getVolume(void);// 返回體積 };

    5.2.2? 類的聲明

    Box box1; Box box2 = Box(parameters); Box box3(parameters); Box* box4 = new Box(parameters);

    5.2.3? 訪問類的成員

    box1.length = 5.0; cout << box1.length << endl;

    5.2.4? 類成員函數

    成員函數可以定義在類定義內部,或者單獨使用范圍解析運算符 :: 來定義。

    class Box {public:double length; // 長度double breadth; // 寬度double height; // 高度double getVolume(void){return length * breadth * height;} }; //您也可以在類的外部使用范圍解析運算符 :: 定義該函數 double Box::getVolume(void) {return length * breadth * height; }//調用成員函數同樣是在對象上使用點運算符(.) Box myBox; // 創建一個對象 myBox.getVolume(); // 調用該對象的成員函數

    5.2.5? 類訪問修飾符

    數據封裝是面向對象編程的一個重要特點,它防止函數直接訪問類的內部成員。類成員的訪問限制是通過在類主體內部對各個區域標記 public、private、protected 來指定的。關鍵字 public、private、protected 稱為訪問修飾符。

    class Base {public:// 公有成員protected:// 受保護成員private:// 私有成員 };
    • 公有成員在程序中類的外部是可訪問的。
    • 私有成員變量或函數在類的外部是不可訪問的,甚至是不可查看的。只有類和友元函數可以訪問私有成員。如果沒有使用任何訪問修飾符,類的成員將被假定為私有成員
    • 保護成員變量或函數與私有成員十分相似,但有一點不同,保護成員在派生類(即子類)中是可訪問的。

    5.2.6? 類的特殊函數

    1)構造函數

    類的構造函數是類的一種特殊的成員函數,它會在每次創建類的新對象時執行。構造函數的名稱與類的名稱是完全相同的,并且不會返回任何類型,也不會返回 void。

    2)析構函數

    類的析構函數是類的一種特殊成員函數,它會在每次刪除所對象時執行。析構函數的名稱與類的名稱是完全相同的,只是在前面加了個波浪號(~)作為前綴,它不會返回任何值,也不能帶有任何參數。析構函數有助于在跳出程序(比如關閉文件、釋放內存等)前釋放資源。

    class Line {public:void setLength( double len );double getLength( void );Line(); // 這是構造函數Line(double len); // 這是帶參數的構造函數~Line(); // 這是析構函數聲明private:double length; };// 成員函數定義,包括構造函數 Line::Line(void) {cout << "Object is being created" << endl; }Line::Line( double len) {cout << "Object is being created, length = " << len << endl;length = len; }Line::~Line(void) {cout << "Object is being deleted" << endl;delete ptr; }

    3)拷貝構造函數

    拷貝構造函數是一種特殊的構造函數,通常用于:通過使用另一個同類型的對象來初始化新創建的對象。

    class Line {public:int getLength( void );Line( int len ); // 簡單的構造函數Line( const Line &obj); // 拷貝構造函數~Line(); // 析構函數private:int *ptr; };Line::Line(const Line &obj) {cout << "調用拷貝構造函數并為指針 ptr 分配內存" << endl;ptr = new int;*ptr = *obj.ptr; // 拷貝值 }

    4)友元函數

    類的友元函數是定義在類外部,但有權訪問類的所有私有(private)成員和保護(protected)成員。盡管友元函數的原型有在類的定義中出現過,但是友元函數并不是成員函數。

    class Box {double width; public:friend void printWidth( Box box );void setWidth( double wid ); };// 請注意:printWidth() 不是任何類的成員函數 void printWidth( Box box ) {/* 因為 printWidth() 是 Box 的友元,它可以直接訪問該類的任何成員 */cout << "Width of box : " << box.width <<endl; }

    5.2.7? this指針

    在 C++ 中,每一個對象都能通過 this 指針來訪問自己的地址。this 指針是所有成員函數的隱含參數。因此,在成員函數內部,它可以用來指向調用對象。

    class Box{public:Box(){;}~Box(){;}Box* get_address() //得到this的地址{return this;}double Volume(){return length * breadth * height;}int compare(Box box){//指針通過->訪問類成員,對象通過.訪問類成員return this->Volume() > box.Volume();} };

    注:友元函數沒有 this 指針,因為友元不是類的成員。只有成員函數才有 this 指針。

    5.2.8? 指向類的指針

    int main(void) {Box Box1(3.3, 1.2, 1.5); // Declare box1Box Box2(8.5, 6.0, 2.0); // Declare box2Box *ptrBox; // Declare pointer to a class. // 其中ptrBox為地址,*表示從其地址取值// 保存第一個對象的地址ptrBox = &Box1;// 現在嘗試使用成員訪問運算符來訪問成員cout << "Volume of Box1: " << ptrBox->Volume() << endl;// 保存第二個對象的地址ptrBox = &Box2;// 現在嘗試使用成員訪問運算符來訪問成員cout << "Volume of Box2: " << ptrBox->Volume() << endl;return 0; }

    5.2.9? 類的靜態成員

    我們可以使用 static 關鍵字來把類成員定義為靜態的。靜態成員在類的所有對象中是共享的,當我們聲明類的成員為靜態時,這意味著無論創建多少個類的對象,靜態成員都只有一個副本。

    注:如果不存在其他的初始化語句,在創建第一個對象時,所有的靜態數據都會被初始化為零。我們不能把靜態成員的初始化放置在類的定義中,但是可以在類的外部通過使用范圍解析運算符 :: 來重新聲明靜態變量從而對它進行初始化

    class Box {public:static int objectCount;// 構造函數定義Box(double l=2.0, double b=2.0, double h=2.0){cout <<"Constructor called." << endl;length = l;breadth = b;height = h;// 每次創建對象時增加 1objectCount++;}double Volume(){return length * breadth * height;}private:double length; // 長度double breadth; // 寬度double height; // 高度 };// 初始化類 Box 的靜態成員 int Box::objectCount = 1;

    如果把函數成員聲明為靜態的,就可以把函數與類的任何特定對象獨立開來。靜態成員函數即使在類對象不存在的情況下也能被調用,靜態函數只要使用類名加范圍解析運算符 :: 就可以訪問。

    5.3? 數據抽象與封裝

    5.3.1? 定義

    數據抽象是一種僅向用戶暴露接口而把具體的實現細節隱藏起來的機制,是一種依賴于接口實現分離的設計技術。

    數據封裝是一種把數據和操作數據的函數捆綁在一起的機制。

    #include <iostream> using namespace std;class Adder{public:// 構造函數Adder(int i = 0){total = i;}// 對外的接口void addNum(int number){total += number;}// 對外的接口int getTotal(){return total;};private:// 對外隱藏的數據int total; }; int main( ) {Adder a;a.addNum(10);a.addNum(20);a.addNum(30);cout << "Total " << a.getTotal() <<endl;return 0; }

    上面的類把數字相加,并返回總和。公有成員 addNum 和 getTotal 是對外的接口,用戶需要知道它們以便使用類。私有成員 total 是用戶不需要了解的,但又是類能正常工作所必需的。

    數據抽象的好處

    • 類的內部受到保護,不會因無意的用戶級錯誤導致對象狀態受損。
    • 類實現可能隨著時間的推移而發生變化,數據抽象可以更好的取應對不斷變化的需求。

    設計策略

    • 通常情況下,我們都會設置類成員狀態為私有(private),除非我們真的需要將其暴露,這樣才能保證良好的封裝性。抽象把代碼分離為接口和實現。所以在設計組件時,必須保持接口獨立于實現,這樣,如果改變底層實現,接口也將保持不變。在這種情況下,不管任何程序使用接口,接口都不會受到影響,只需要將最新的實現重新編譯即可。

    5.3.2? 接口

    接口描述了類的行為和功能,而不需要完成類的特定實現。如果類中至少有一個函數被聲明為純虛函數,則這個類就是抽象類。

    class Shape {protected:int width, height;public:Shape( int a=0, int b=0){width = a;height = b;}virtual int area(){cout << "Parent class area :" <<endl;return 0;}// pure virtual functionvirtual int area() = 0; };

    設計抽象類(通常稱為 ABC)的目的,是為了給其他類提供一個可以繼承的適當的基類。抽象類不能被用于實例化對象,它只能作為接口使用。因此,如果一個 ABC 的子類需要被實例化,則必須實現每個虛函數,如果沒有在派生類中重寫純虛函數,就嘗試實例化該類的對象,會導致編譯錯誤。可用于實例化對象的類被稱為具體類。

    5.4? 繼承

    繼承代表了 is a 關系。例如,哺乳動物是動物,狗是哺乳動物,因此,狗是動物,等等。一個類可以派生自多個類,這意味著,它可以從多個基類繼承數據和函數。類派生列表以一個或多個基類命名,形式如下:

    class derived-class: access-specifier base-class//例如 #include <iostream>using namespace std;// 基類 Shape class Shape {public:void setWidth(int w){width = w;}void setHeight(int h){height = h;}protected:int width;int height; };// 基類 PaintCost class PaintCost {public:int getCost(int area){return area * 70;} };// 派生類 class Rectangle: public Shape, public PaintCost {public:int getArea(){ return (width * height); } };int main(void) {Rectangle Rect;int area;Rect.setWidth(5);Rect.setHeight(7);area = Rect.getArea();// 輸出對象的面積cout << "Total area: " << Rect.getArea() << endl;// 輸出總花費cout << "Total paint cost: $" << Rect.getCost(area) << endl;return 0; }

    派生類可以訪問基類中所有的非私有成員,同時,一個派生類繼承了所有的基類方法,但下列情況除外:

    • 基類的構造函數、析構函數和拷貝構造函數。
    • 基類的重載運算符。
    • 基類的友元函數。

    5.5? 多態

    5.5.1? 虛函數

    虛函數是在基類中使用關鍵字 virtual 聲明的函數。在派生類中重新定義基類中定義的虛函數時,會告訴編譯器不要靜態鏈接到該函數。我們想要的是在程序中任意點可以根據所調用的對象類型來選擇調用的函數,這種操作被稱為動態鏈接,或后期綁定。

    C++ 多態意味著調用成員函數時,會根據調用函數的對象的類型來執行不同的函數,例如:

    #include <iostream> using namespace std;class Shape {protected:int width, height;public:Shape( int a=0, int b=0){width = a;height = b;}virtual int area(){cout << "Parent class area :" <<endl;return 0;} }; class Rectangle: public Shape{public:Rectangle( int a=0, int b=0):Shape(a, b) { }int area (){ cout << "Rectangle class area :" <<endl;return (width * height); } }; class Triangle: public Shape{public:Triangle( int a=0, int b=0):Shape(a, b) { }int area (){ cout << "Triangle class area :" <<endl;return (width * height / 2); } }; // 程序的主函數 int main( ) {Shape *shape;Rectangle rec(10,7);Triangle tri(10,5);// 存儲矩形的地址shape = &rec;// 調用矩形的求面積函數 areashape->area(); //Rectangle class area// 存儲三角形的地址shape = &tri;// 調用三角形的求面積函數 areashape->area(); //Triangle class areareturn 0; }

    注意:若在基類中不能對虛函數給出有意義的實現,這個時候就會用到純虛函數,在函數參數后直接加 = 0 告訴編譯器,函數沒有主體,這種虛函數即是純虛函數。

    5.6? 相關面試題

    Q:C++中struct和class的區別

    A:struct 更適合看成是一個數據結構的實現體,class 更適合看成是一個對象的實現體。它們最本質的一個區別就是:struct 訪問權限默認是 public 的,class 默認是 private 的。

    Q:析構函數是否需要是虛函數?

    A:只有當一個類需要當作父類時,才將它的析構函數設置為虛函數。

  • 將可能會被繼承的父類的析構函數設置為虛函數,可以保證當我們new一個子類,然后使用基類指針指向該子類對象,釋放基類指針時可以釋放掉子類的空間,防止內存泄漏;
  • C++默認的析構函數不是虛函數,著是因為虛函數需要額外的虛函數表和虛表指針,占用額外的內存。而對于不會被繼承的類來說,其析構函數如果是虛函數,就會浪費內存。
  • Q:C++中析構函數的特點

    A:

    • 當對象結束其生命周期,如對象所在的函數已調用完畢時,系統會自動執行析構函數;
    • 析構函數名與類名相同,只是在函數名前面加一個位取反符~,只能有一個析構函數,不能重載;
    • 如果用戶沒有編寫析構函數,編譯系統會自動生成一個缺省的析構函數;
    • 如果一個類中有指針,且在使用的過程中動態的申請了內存,那么最好顯式構造析構函數,在銷毀類之前釋放掉申請的內存空間,避免內存泄漏;
    • 類析構順序:1)派生類本身的析構函數;2)對象成員析構函數;3)基類析構函數。

    Q:靜態函數和虛函數的區別

    A:

    • 靜態函數在編譯的時候就已經確定運行時機,虛函數在運行的時候動態綁定。
    • 虛函數因為用了虛函數表機制,調用的時候會增加一次內存開銷。

    Q:必須使用成員初始化列表的場合

    A:初始化列表的好處:少了一次調用默認構造函數的過程,提高了效率。

    有些場合必須要用初始化列表:

    • 常量成員,因為常量只能初始化不能賦值,所以必須放在初始化列表里面
    • 引用類型,引用必須在定義的時候初始化,并且不能重新賦值,所以也要寫在初始化列表里面
    • 沒有默認構造函數的類類型,因為使用初始化列表可以不必調用默認構造函數來初始化

    Q:面向對象三大特征

    A:

    • 封裝:把數據和操作綁定在一起封裝成抽象的類,僅向用戶暴露接口,而對其隱藏具體實現,以此避免外界干擾和不確定性訪問;
    • 繼承:讓某種類型對象獲得另一個類型對象的屬性和方法,提高了代碼的可維護性;
    • 多態:讓同一事物體現出不同事物的狀態,提高了代碼的擴展性。

    Q:C++ 多態分類及實現

    A:

    • 重載多態(Ad-hoc Polymorphism,編譯期):函數重載、運算符重載(靜態多態)
    • 子類型多態(Subtype Polymorphism,運行期):虛函數(動態多態)
    • 參數多態(Parametric Polymorphism,編譯期):類模板、函數模板
    • 強制多態(Coercion Polymorphism,編譯期/運行期):基本類型轉換、自定義類型轉換

    Q:是不是一個父類寫了一個virtual 函數,如果子類重寫它的函數不加virtual ,也能實現多態?

    A:virtual修飾符會被隱形繼承,因此可加可不加,子類覆蓋它的函數不加virtual,也能實現多態。

    Q:虛表指針、虛函數指針和虛函數表

    A:

    • 虛表指針:在含有虛函數的類的對象中,指向虛函數表的指針,在運行時確定。
    • 虛函數指針:指向虛函數的地址的指針。
    • 虛函數表:在程序只讀數據段,存放虛函數指針,如果派生類實現了基類的某個虛函數,則在虛函數表中覆蓋原本基類的那個虛函數指針,在編譯時根據類的聲明創建。

    Q:簡單描述虛繼承與虛基類?

    A:定義:在C++中,在定義公共基類A的派生類B、C...的時候,如果在繼承方式前使用關鍵字virtual對繼承方式限定,這樣的繼承方式就是虛擬繼承,公共基類A成為虛基類。這樣,在具有公共基類的、使用了虛擬繼承方式的多個派生類B、C...的公共派生類D中,該基類A的成員就只有一份拷貝。

    ?作用:一個類有多個基類,這樣的繼承關系稱為多繼承。在多繼承的情況下,如果不同基類的成員名稱相同,匹配度相同, 則會造成二義性。為了避免多繼承產生的二義性,在這種機制下,不論虛基類在繼承體系中出現了多少次,在派生類中都只包含一份虛基類的成員。

    Q:抽象類、接口類、聚合類

    A:

    • 抽象類:含有純虛函數的類
    • 接口類:僅含有純虛函數的抽象類
    • 聚合類:用戶可以直接訪問其成員,并且具有特殊的初始化語法形式。滿足如下特點:1)所有成員都是 public;2)沒有定義任何構造函數;3)沒有類內初始化;4)沒有基類,也沒有 virtual 函數

    Q:如何定義一個只能在堆上(棧上)生成對象的類?

    A:

    • 只能在堆上
      • 方法: 將析構函數設置為私有
      • 原因:C++ 是靜態綁定語言,編譯器管理棧上對象的生命周期,編譯器在為類對象分配??臻g時,會先檢查類的析構函數的訪問性。若析構函數不可訪問,則不能在棧上創建對象。
    • 只能在棧上
      • 方法:將 new 和 delete 重載為私有
      • 原因: 在堆上生成對象,使用 new 關鍵詞操作,其過程分為兩階段:第一階段,使用 new 在堆上尋找可用內存,分配給對象;第二階段,調用構造函數生成對象。將 new 操作設置為私有,那么第一階段就無法完成,就不能夠在堆上生成對象。

    Q:構造函數是否可以用private修飾,如果可以,會有什么效果?

    A:如果一個類的構造函數只有一個且為private:

    • 可以編譯通過;
    • 如果類的內部沒有專門創建實例的代碼,則是無法創建任何實例的;
    • 如果類的內部有專門創建實例的代碼,則只能創建一個或多個實例(根據類內部聲明的成員對象個數來定);
    • private 構造函數如果參數 為void(無參),則子類無法編譯。

    Q:子類的指針能否轉換為父類的指針?父類指針能否訪問子類成員?

    A:首先要明確,當一個父類指針指向子類對象時是安全的,但只能訪問從父類繼承的成員;然而當一個子類指針指向父類對象時,因為可能調用父類不存在的方法,所以是不安全的,會爆語法錯誤。

    • 當自己的類指針指向自己類的對象時,無論調用的是虛函數還是實函數,其調用的都是自己的;
    • 當指向父類對象的父類指針被強制轉換成子類指針時,也就是子類指針指向父類對象,此時,子類指針調用函數時,只有非重寫函數是自己的,虛函數是父類的;
    • 當指向子類對象的子類指針被強制轉換成父類指針時,也就是父類指針指向子類對象,此時,父類指針調用的虛函數都是子類的,而非虛函數都是自己的。

    Q:this 指針

    A:this 指針是一個隱含于每一個非靜態成員函數中的特殊指針,它指向調用該成員函數的對象的首地址。

    • 當對一個對象調用成員函數時,編譯程序先將對象的地址賦給 this 指針,然后調用成員函數,每次成員函數存取數據成員時,都隱式使用 this 指針。
    • this 指針被隱含地聲明為: ClassName *const this,這意味著不能給?this 指針賦值。
    • this 并是個右值,所以不能取 this 的地址。

    Q:delete this

    A:

    • 類的成員函數中可以調用delete this,但是在釋放后,對象后續調用的方法不能再用到this指針;
    • delete this釋放了類對象的內存空間,但是內存空間卻并不是馬上被回收到系統中,此時其中的值是不確定的;
    • delete的本質是為將被釋放的內存調用一個或多個析構函數,如果在類的析構函數中調用delete this,會陷入無限遞歸,造成棧溢出。

    Q:一個空類class中有什么?

    A:構造函數、拷貝構造函數、析構函數、賦值運算符重載、取地址操作符重載、被const修飾的取地址操作符重載

    Q:C++計算一個類的sizeof

    A:

    • 一個空的類sizeof返回1,因為一個空類也要實例化,所謂類的實例化就是在內存中分配一塊地址;
    • 類內的普通成員函數不參與sizeof的統計,因為sizeof是針對實例的,而普通成員函數,是針對類體的;
    • 一個類如果含有虛函數,則這個類中有一個指向虛函數表的指針,占4個字節;
    • 靜態成員不影響類的大小,被編譯器放在程序的數據段中;
    • 普通繼承的類sizeof,會得到基類的大小加上派生類自身成員的大小;
    • 當存在虛擬繼承時,派生類中會有一個指向虛基類表的指針。所以其大小應為普通繼承的大小,再加上虛基類表的指針大小。

    Q:構造函數和析構函數能被繼承嗎?

    A:不能。構造函數和析構函數是用來處理對象的創建和析構的,它們只知道對在它們的特殊層次的對象做什么。

    Q:構造函數能不能是虛函數?

    A:不能。虛函數對應一個虛函數表,可是這個虛函數表存儲在對象的內存空間的。問題就在于,如果構造函數是虛的,就需要通過 虛函數表來調用,可是對象還沒有實例化,也就是內存空間還沒有,就不會有虛函數表。

    Q:構造函數和析構函數能不能被重載 ?

    A:構造函數可以被重載,析構函數不可以被重載。因為構造函數可以有多個且可以帶參數, 而析構函數只能有一個,且不能帶參數。

    Q:構造函數調用順序,析構函數呢?

    A:基類的構造函數——成員類對象的構造函數——派生類的構造函數;析構函數相反。

    Q:構造函數和析構函數調用時機?

    A:

  • 全局范圍中的對象:構造函數在所有函數調用之前執行,在主函數執行完調用析構函數。
  • 局部自動對象:建立對象時調用構造函數,函數結束時調用析構函數。
  • 動態分配的對象:建立對象時調用構造函數,調用釋放時調用析構函數。
  • 靜態局部變量對象:建立對象時調用構造函數,在主函數結束時調用析構函數。
  • Q:拷貝構造函數中深拷貝和淺拷貝區別?

    A:

  • 深拷貝會先申請一塊和拷貝數據一樣大的內存空間,然后將數據逐字節拷貝過去,拷貝后兩個指針指向不同的兩個內存空間;
  • 淺拷貝僅是拷貝指針地址,拷貝后兩個指針指向同一個內存空間。
  • 當淺拷貝時,如果原來的對象調用析構函數釋放掉指針所指向的數據,則會產生空懸指針,因為所指向的內存空間已經被釋放了。

    Q:拷貝構造函數在什么時候會被調用?

    A:

  • 當用類的一個對象去初始化該類的另一個對象(或引用)時系統自動調用拷貝構造函數實現拷貝賦值;
  • 若函數的形參為類對象,調用函數時,實參賦值給形參,系統自動調用拷貝構造函數;
  • 當函數的返回值是類對象時,系統自動調用拷貝構造函數;
  • 需要產生一個臨時類對象時。
  • Q:什么時候必須重寫拷貝構造函數?

    A:當構造函數涉及到動態內存分配時,要自己寫拷貝構造函數,并且要深拷貝。

    Q:什么是常對象?

    A:常對象是指在任何場合都不能對其成員的值進行修改的對象。

    Q:面向過程編程和面向對象編程的區別

    A:面向過程:就是分析出解決問題所需要的步驟,然后用函數把這些步驟一步一步實現。

    面向對象:面向對象是一種對現實世界理解和抽象的方法,強調的是通過將需求要素轉化為對象進行問題處理的一種思想。

    Q:為什么內聯函數,構造函數,靜態成員函數不能為virtual函數?

    A:

  • 內聯函數
    內聯函數是在編譯時期展開,而虛函數的特性是運行時才動態聯編,所以兩者矛盾,不能定義內聯函數為虛函數。

  • 構造函數
    構造函數用來創建一個新的對象,而虛函數的運行是建立在對象的基礎上,在構造函數執行時,對象尚未形成,所以不能將構造函數定義為虛函數。

  • 靜態成員函數
    靜態成員函數屬于一個類而非某一對象,沒有this指針,它無法進行對象的判別。

  • 友元函數
    C++不支持友元函數的繼承,對于沒有繼承性的函數沒有虛函數。

  • Q:如何定義和實現一個類的成員函數為回調函數?

    A:所謂的回調函數,就是預先在系統的對函數進行注冊,讓系統知道這個函數的存在,以后,當某個事件發生時,再調用這個函數對事件進行響應。

    定義一個類的成員函數時在該函數前加CALLBACK即將其定義為回調函數,函數的實現和普通成員函數沒有區別。

    ?

    第六章? 動態內存

    C++ 程序中的內存分為兩個部分:

    • 棧:在函數內部聲明的所有變量都將占用棧內存。
    • 堆:這是程序中未使用的內存,在程序運行時可用于動態分配內存。

    6.1? new和delete運算符

    在 C++ 中,可以使用特殊的運算符為給定類型的變量在運行時分配堆內的內存,這會返回所分配的空間地址。這種運算符即?new?運算符。如果不再需要動態分配的內存空間,可以使用?delete?運算符,刪除之前由 new 運算符分配的內存。

    #include <iostream> using namespace std;int main () {double* pvalue = NULL; // 初始化為 null 的指針pvalue = new double; // 為變量請求內存*pvalue = 29494.99; // 在分配的地址存儲值cout << "Value of pvalue : " << *pvalue << endl;delete pvalue; // 釋放內存return 0; }

    6.2? 動態內存分配

    6.2.1? 數組的動態內存分配

    假設我們要為一個字符數組(一個有 20 個字符的字符串)分配內存,我們可以使用上面實例中的語法來為數組動態地分配內存:

    char* pvalue = NULL; // 初始化為 null 的指針 pvalue = new char[20]; // 為變量請求內存

    要刪除我們剛才創建的數組,語句如下:

    delete [] pvalue; // 刪除 pvalue 所指向的數組

    二維數組示例:

    #include <iostream> using namespace std;int main() {int **p; int i,j; //p[4][8] //開始分配4行8列的二維數據 p = new int *[4];for(i=0;i<4;i++){p[i]=new int [8];}for(i=0; i<4; i++){for(j=0; j<8; j++){p[i][j] = j*i;}} //打印數據 for(i=0; i<4; i++){for(j=0; j<8; j++) { if(j==0) cout<<endl; cout<<p[i][j]<<"\t"; }} //開始釋放申請的堆 for(i=0; i<4; i++){delete [] p[i]; }delete [] p; return 0; }

    6.2.2? 對象的動態內存分配

    對象與簡單的數據類型沒有什么不同:

    #include <iostream> using namespace std;class Box {public:Box() { cout << "調用構造函數!" <<endl; }~Box() { cout << "調用析構函數!" <<endl; } };int main( ) {Box* myBoxArray = new Box[4];delete [] myBoxArray; // 刪除數組return 0; }

    如果要為一個包含四個 Box 對象的數組分配內存,構造函數將被調用 4 次,同樣地,當刪除這些對象時,析構函數也將被調用相同的次數。

    6.3? 相關面試題

    Q:new/delete具體步驟

    A:使用new操作符來分配對象內存時會經歷三個步驟:

    • 第一步:調用operator new 函數分配一塊足夠大的,原始的,未命名的內存空間以便存儲特定類型的對象。
    • 第二步:編譯器運行相應的構造函數以構造對象,并為其傳入初值。
    • 第三部:對象構造完成后,返回一個指向該對象的指針。

    使用delete操作符來釋放對象內存時會經歷兩個步驟:

    • 第一步:調用對象的析構函數。
    • 第二步:編譯器調用operator delete函數釋放內存空間。

    Q:new/delete與malloc/free的區別是什么?

    A:

    • malloc/free是C語言的標準庫函數, new/delete是C++的運算符。它們都可用于申請動態內存和釋放內存;
    • malloc/free不會去自動調用構造和析構函數,對于基本數據類型的對象而言,光用malloc/free無法滿足動態對象的要求;
    • malloc/free需要指定分配內存的大小,而new/delete會自動計算所需內存大小;
    • new返回的是指定對象的指針,而malloc返回的是void*,因此malloc的返回值一般都需要進行強制類型轉換。

    Q:C++內存管理

    A:在C++中,虛擬內存分為代碼段、數據段、BSS段、堆區、棧區以及文件映射區六部分。

    • 代碼段:包括只讀存儲區和文本區,其中只讀存儲區存儲字符串常量,文本區存儲程序的機器代碼。
    • 數據段:存儲程序中已初始化的全局變量和靜態變量
    • BSS段:存儲未初始化的全局變量和靜態變量(局部+全局),以及所有被初始化為0的全局變量和靜態變量。
    • 堆區:調用new/malloc函數時在堆區動態分配內存,同時需要調用delete/free來手動釋放申請的內存。
    • 映射區:存儲動態鏈接庫以及調用mmap函數進行的文件映射
    • 棧區:使用??臻g存儲函數的返回地址、參數、局部變量、返回值

    Q:內存的分配方式

    A:內存分配方式有三種:

  • 靜態存儲區,是在程序編譯時就已經分配好的,在整個運行期間都存在,如全局變量、常量。
  • 棧上分配,函數內的局部變量就是從這分配的,但分配的內存容易有限。
  • 堆上分配,也稱動態分配,如我們用new,malloc分配內存,用delete,free來釋放的內存。
  • Q:簡單介紹內存池?

    A:內存池是一種內存分配方式。通常我們習慣直接使用new、malloc申請內存,這樣做的缺點在于所申請內存塊的大小不定,當頻繁使用時會造成大量的內存碎片并進而降低性能。內存池則是在真正使用內存之前,預先申請分配一定數量、大小相等(一般情況下)的內存塊留作備用。當有新的內存需求時,就從內存池中分出一部分內存塊,若內存塊不夠再繼續申請新的內存。這樣做的一個顯著優點是,使得內存分配效率得到提升。

    Q:簡單描述內存泄漏?

    A:內存泄漏一般是指堆內存的泄漏,也就是程序在運行過程中動態申請的內存空間不再使用后沒有及時釋放,導致那塊內存不能被再次使用。

    Q:C++中的不安全是什么概念?

    A:C++中的不安全包括兩種:一是程序得不到正確的結果,二是發生不可預知的錯誤(占用了不該用的內存空間)。可能會發生如下問題:

  • 最嚴重的:內存泄漏,程序崩潰;
  • 一般嚴重的:發生一些邏輯錯誤,且不便于調試;
  • 較輕的:丟失部分數據,就像強制轉換一樣。
  • Q:內存中的堆與棧有什么區別?

    A:

  • 申請方式:棧由系統自動分配和管理,堆由程序員手動分配和管理。
  • 效率:棧由系統分配,計算機底層對棧提供了一系列支持:分配專門的寄存器存儲棧的地址,壓棧和入棧有專門的指令執行,因此,其速度快,不會有內存碎片;堆由程序員分配,堆是由C/C++函數庫提供的,機制復雜,需要一些列分配內存、合并內存和釋放內存的算法,因此效率較低,可能由于操作不當產生內存碎片。
  • 擴展方向:棧從高地址向低地址進行擴展,堆由低地址向高地址進行擴展。
  • 程序局部變量是使用的??臻g,new/malloc動態申請的內存是堆空間;同時,函數調用時會進行形參和返回值的壓棧出棧,也是用的??臻g。
  • ?

    第七章? C++ STL(標準模板庫)

    STL(Standard Template Library),即標準模板庫,是一個具有工業強度的,高效的C++程序庫。STL中包括六大組件:容器、迭代器、算法、仿函數、迭代適配器、空間配置器。

    7.1? 容器

    STL中的常用容器包括:序列式容器(vector、deque、list)、關聯式容器(map、set)、容器適配器(queue、stack)。

    7.1.1 序列式容器

    1)vector

    vector是一種動態數組,在內存中具有連續的存儲空間,支持快速隨機訪問。由于具有連續的存儲空間,所以在插入和刪除操作方面,效率比較慢。其常用操作如下:

    //需要包含頭文件 #include <vector>//1.定義和初始化 vector<int> vec1; //默認初始化,vec1為空 vector<int> vec2(vec1); //使用vec1初始化vec2 vector<int> vec3(vec1.begin(),vec1.end());//使用vec1初始化vec2 vector<int> vec4(10); //10個值為0的元素 vector<int> vec5(10,4); //10個值為4的元素//2.常用操作方法 //2.1 添加函數 vec1.push_back(100); //尾部添加元素 vec1.insert(vec1.end(),5,3); //從vec1.back位置插入5個值為3的元素//2.2 刪除函數 vec1.pop_back(); //刪除末尾元素 vec1.erase(vec1.begin(),vec1.begin()+2); //刪除vec1[0]-vec1[2]之間的元素,不包括vec1[2]其他元素前移 vec1.clear(); //清空元素,元素在內存中并未消失,通常使用swap()來清空 vector<int>().swap(V); //利用swap函數和臨時對象交換內存,交換以后,臨時對象消失,釋放內存。//2.3 遍歷函數 vec1[0]; //取得第一個元素 vec1.at(int pos); //返回pos位置元素的引用 vec1.front(); //返回首元素的引用 vec1.back(); //返回尾元素的引用 vector<int>::iterator begin= vec1.begin(); //返回向量頭指針,指向第一個元素 vector<int>::iterator end= vec1.end(); //返回向量尾指針,指向向量最后一個元素的下一個位置 vector<int>::iterator rbegin= vec1.rbegin(); //反向迭代器,指向最后一個元素 vector<int>::iterator rend= vec1.rend(); //反向迭代器,指向第一個元素之前的位置//2.4 判斷函數 bool isEmpty = vec1.empty(); //判斷是否為空//2.5 大小函數 int size = vec1.size(); //元素個數 vec1.capacity(); //返回容器當前能夠容納的元素個數 vec1.max_size(); //返回容器最大的可能存儲的元素個數//2.6 改動函數 vec1.assign(int n,const T& x); //賦n個值為x的元素到vec1中,這會清除掉vec1中以前的內容。 vec1.assign(const_iterator first,const_iterator last); //當前向量中[first,last)中元素設置成迭代器所指向量的元素,這會清除掉vec1中以前的內容。

    2)deque

    所謂的deque是”double ended queue”的縮寫,雙向隊列不論在尾部或頭部插入元素,都十分迅速。而在中間插入元素則會比較費時,因為必須移動中間其他的元素。

    #include <deque> // 頭文件//1.聲明和初始化 deque<type> deq; // 聲明一個元素類型為type的雙端隊列que deque<type> deq(size); // 聲明一個類型為type、含有size個默認值初始化元素的的雙端隊列que deque<type> deq(size, value); // 聲明一個元素類型為type、含有size個value元素的雙端隊列que deque<type> deq(mydeque); // deq是mydeque的一個副本 deque<type> deq(first, last); // 使用迭代器first、last范圍內的元素初始化deq//2.常用成員函數 deq[index]; //用來訪問雙向隊列中單個的元素。 deq.at(index); //用來訪問雙向隊列中單個的元素。 deq.front(); //返回第一個元素的引用。 deq.back(); //返回最后一個元素的引用。 deq.push_front(x); //把元素x插入到雙向隊列的頭部。 deq.pop_front(); //彈出雙向隊列的第一個元素。 deq.push_back(x); //把元素x插入到雙向隊列的尾部。 deq.pop_back(); //彈出雙向隊列的最后一個元素。

    3)list

    list是STL實現的雙向鏈表,與vector相比, 它允許快速的插入和刪除,但是隨機訪問卻比較慢。

    #include <list>//1.定義和初始化 list<int>lst1; //創建空list list<int> lst2(5); //創建含有5個元素的list list<int>lst3(3,2); //創建含有3個元素值為2的list list<int>lst4(lst2); //使用lst2初始化lst4 list<int>lst5(lst2.begin(),lst2.end()); //同lst4//2.常用操作函數 lst1.assign(lst2.begin(),lst2.end()); //給list賦值為lst2 lst1.back(); //返回最后一個元素 lst1.begin(); //返回指向第一個元素的迭代器 lst1.clear(); //刪除所有元素 lst1.empty(); //如果list是空的則返回true lst1.end(); //返回末尾的迭代器 lst1.erase(); //刪除一個元素 lst1.front(); //返回第一個元素 lst1.insert(); //插入一個元素到list中 lst1.max_size(); //返回list能容納的最大元素數量 lst1.merge(); //合并兩個list lst1.pop_back(); //刪除最后一個元素 lst1.pop_front(); //刪除第一個元素 lst1.push_back(); //在list的末尾添加一個元素 lst1.push_front(); //在list的頭部添加一個元素 lst1.rbegin(); //返回指向第一個元素的逆向迭代器 lst1.remove(); //從list刪除元素 lst1.remove_if(); //按指定條件刪除元素 lst1.rend(); //指向list末尾的逆向迭代器 lst1.resize(); //改變list的大小 lst1.reverse(); //把list的元素倒轉 lst1.size(); //返回list中的元素個數 lst1.sort(); //給list排序 lst1.splice(); //合并兩個list lst1.swap(); //交換兩個list lst1.unique(); //刪除list中相鄰重復的元素

    7.1.2? 關聯式容器

    1)map

    map是STL的一個關聯容器,它是一種鍵值對容器,里面的數據都是成對出現的,可在我們處理一對一數據的時候,在編程上提供快速通道。map內部自建一顆紅黑樹(一種非嚴格意義上的平衡二叉樹),這顆樹具有對數據自動排序的功能,所以在map內部所有的數據都是有序的。

    #include <map>//1.定義與初始化 map<int, string> ID_Name; // 使用{}賦值是從c++11開始的,因此編譯器版本過低時會報錯,如visual studio 2012 map<int, string> ID_Name = {{ 2015, "Jim" },{ 2016, "Tom" },{ 2017, "Bob"}};//2.基本操作函數 count() //返回指定元素出現的次數 find() //查找一個元素 get_allocator() //返回map的配置器 key_comp() //返回比較元素key的函數 lower_bound() //返回鍵值>=給定元素的第一個位置 upper_bound() //返回鍵值>給定元素的第一個位置 value_comp() //返回比較元素value的函數 map<int,string>::iterator iter_map = map1.begin();//取得迭代器首地址 int key = iter_map->first; //取得key string value = iter_map->second; //取得value

    2)set

    set的含義是集合,它是一個有序的容器,里面的元素都是排序好的支持插入、刪除、查找等操作,就像一個集合一樣,所有的操作都是嚴格在logn時間內完成,效率非常高,使用方法類似list。

    7.2? 相關面試題

    Q:六大組件介紹

    A:容器:數據結構,用來存放數據
    算法:常用算法
    迭代器:容器和算法之間的膠合劑,“范型指針”
    仿函數:一種重載了operator()的類,使得這個類的使用看上去像一個函數
    配置器:為容器分配并管理內存
    適配器:修改其他組件接口

    Q:STL常用的容器有哪些以及各自的特點是什么?

    A:

    • vector:底層數據結構為數組 ,支持快速隨機訪問。
    • list:底層數據結構為雙向鏈表,支持快速增刪。
    • deque:底層數據結構為一個中央控制器和多個緩沖區,支持首尾(中間不能)快速增刪,也支持隨機訪問。
    • stack:底層一般用deque/list實現,封閉頭部即可,不用vector的原因應該是容量大小有限制,擴容耗時。
    • queue:底層一般用deque/list實現,封閉頭部即可,不用vector的原因應該是容量大小有限制,擴容耗時。
    • priority_queue:的底層數據結構一般為vector為底層容器,堆heap為處理規則來管理底層容器實現。
    • set:底層數據結構為紅黑樹,有序,不重復。
    • multiset:底層數據結構為紅黑樹,有序,可重復。
    • map:底層數據結構為紅黑樹,有序,不重復。
    • multimap:底層數據結構為紅黑樹,有序,可重復。
    • unordered_set:底層數據結構為hash表,無序,不重復。
    • unordered_multiset:底層數據結構為hash表,無序,可重復 。
    • unordered_map :底層數據結構為hash表,無序,不重復。
    • unordered_multimap:底層數據結構為hash表,無序,可重復。

    Q:說說?vector 和 list 的區別

    A:

  • vector底層實現是數組,所以在內存中是連續存放的,隨機讀取效率高,但插入、刪除效率低;list底層實現是雙向鏈表,所以在內存中是任意存放的,插入、刪除效率高,但訪問元素效率低。
  • vector在中間節點進行插入、刪除會導致內存拷貝,而list不會。
  • vector一次性分配好內存,不夠時才進行2倍擴容;list每次插入新節點都會進行內存申請。
  • Q:vector擴容原理

    A:以原內存空間大小的兩倍配置一份新的內存空間,并將原空間數據拷貝過來進行初始化。

    Q:map 和 set 有什么區別

    A:

  • ?map中的元素是鍵值對;Set僅是關鍵字的簡單集合;
  • set的迭代器是const的,不允許修改元素的值;map允許修改value,但不允許修改key;
  • map支持用關鍵字作下標操作,set不支持下標操作。
  • Q:map和unordered_map的區別

    A:

    • map: map內部實現了一個紅黑樹,紅黑樹的每一個節點都代表著map的一個元素,因此所有元素都是有序的,對其進行查找、插入、刪除得效率都是O(log n);但是,因為每個結點都需要額外保存數據,所以空間占用率比較高。
    • unordered_map: unordered_map內部實現了一個哈希表,因此內部元素是無序的,對其進行查找、插入、刪除得效率都是O(1);但是建立哈希表比較費時。

    Q:STL 中迭代器的作用,有指針為何還要迭代器

    A:

    • Iterator(迭代器)模式又稱Cursor(游標)模式,用于提供一種方法順序訪問一個聚合對象中各個元素, 而又不需暴露該對象的內部表示。
    • 迭代器不是指針,是類模板,表現的像指針。他只是模擬了指針的一些功能,通過重載了指針的一些操作符,->、*、++、--等,相當于一種智能指針。
    • 迭代器產生原因:Iterator采用的是面向對象的思想,把不同集合類的訪問邏輯抽象出來,使得不用暴露集合內部的結構而達到循環遍歷集合的效果。

    ?

    ?

    第八章? 異常處理

    C++ 異常處理涉及到三個關鍵字:try、catch、throw。

    • throw: 當問題出現時,程序會拋出一個異常。這是通過使用 throw 關鍵字來完成的。
    • catch: 在想要處理問題的地方,通過異常處理程序捕獲異常。catch 關鍵字用于捕獲異常。
    • try: try 塊中放置可能拋出異常的代碼,try 塊中的代碼被稱為保護代碼。它后面通常跟著一個或多個 catch 塊。

    8.1? 拋出異常

    可以使用 throw 語句在代碼塊中的任何地方拋出異常。throw 語句的操作數可以是任意的表達式,表達式的結果的類型決定了拋出的異常的類型。

    double division(int a, int b) {if( b == 0 ){throw "Division by zero condition!";}return (a/b); }

    8.2? 捕獲異常

    try {// 保護代碼 }catch( ExceptionName e1 ) {// catch 塊 }catch( ExceptionName e2 ) {// catch 塊 }catch( ExceptionName eN ) {// catch 塊 }

    上面的代碼會捕獲一個類型為 ExceptionName 的異常。如果想讓 catch 塊能夠處理 try 塊拋出的任何類型的異常,則必須在異常聲明的括號內使用省略號 ...,例如:

    try {// 保護代碼 }catch(...) {// 能處理任何異常的代碼 }

    8.3? C++標準的異常

    C++ 提供了一系列標準的異常,定義在 <exception> 中,我們可以在程序中使用這些標準的異常。

    8.4? 定義新的異常

    可以通過繼承和重載 exception 類來定義新的異常。

    #include <iostream> #include <exception> using namespace std;struct MyException : public exception {const char * what () const throw (){return "C++ Exception";} };int main() {try{throw MyException();}catch(MyException& e){std::cout << "MyException caught" << std::endl;std::cout << e.what() << std::endl;}catch(std::exception& e){//其他的錯誤} }

    ?

    第九章? 多線程

    多線程是多任務處理的一種特殊形式,一般情況下,有基于進程和基于線程的兩種類型的多任務處理方式。

    • 基于進程的多任務處理是程序的并發執行。
    • 基于線程的多任務處理是同一程序的片段的并發執行。

    9.1? 基本概念

    9.1.1? 進程與線程

    進程是資源分配和調度的一個獨立單位;而線程是進程的一個實體,是CPU調度和分配的基本單位。

    同一個進程中的多個線程的內存資源是共享的,各線程都可以改變進程中的變量。因此在執行多線程運算的時候要注意執行順序。

    9.1.2? 并行與并發

    并行(parallellism)指的是多個任務在同一時刻同時在執行。

    并發(concurrency)是指在一個時間段內,多個任務交替進行。雖然看起來像在同時執行,但其實是交替的。

    9.2? C++線程管理

    • C++11的標準庫中提供了多線程庫,使用時需要#include <thread>頭文件,該頭文件主要包含了對線程的管理類std::thread以及其他管理線程相關的類。
    • 每個應用程序至少有一個進程,而每個進程至少有一個主線程,除了主線程外,在一個進程中還可以創建多個子線程。每個線程都需要一個入口函數,入口函數返回退出,該線程也會退出,主線程就是以main函數作為入口函數的線程。

    9.2.1? 啟動線程

    std::thread的構造函數需要的是可調用(callable)類型,除了函數外,還可以調用例如:lambda表達式、重載了()運算符的類的實例。

    #include <iostream> #include <thread>using namespace std;void output(int i) {cout << i << endl; }int main() {for (uint8_t i = 0; i < 4; i++){//創建一個線程t,第一個參數為調用的函數,第二個參數為傳遞的參數thread t(output, i);//表示允許該線程在后臺運行t.detach(); }return 0; }

    在多線程并行的條件下,其輸出結果不一定是順序呢的輸出1234,可能如下:

    多線程并行

    ?

    注意:

    • 把函數對象傳入std::thread時,應傳入函數名稱(命名變量,如:output)而不加括號(臨時變量,如:output())。
    • 當啟動一個線程后,一定要在該線程thread銷毀前,調用t.join()或者t.detach(),確定以何種方式等待線程執行結束:
      • detach方式,啟動的線程自主在后臺運行,當前的代碼繼續往下執行,不等待新線程結束。
      • join方式,等待關聯的線程完成,才會繼續執行join()后的代碼。
      • 在以detach的方式執行線程時,要將線程訪問的局部數據復制到線程的空間(使用按值傳遞),一定要確保線程沒有使用局部變量的引用或者指針,除非你能肯定該線程會在局部作用域結束前執行結束。

    9.2.2? 向線程傳遞參數

    向線程調用的函數只需要在構造thread的實例時,依次傳入即可。

    thread t(output, arg1, arg2, arg3, ...);

    9.2.3? 調用類成員函數

    class foo { public:void bar1(int n){cout<<"n = "<<n<<endl;}static void bar2(int n){cout<<"static function is running"<<endl;cout<<"n = "<<n<<endl;} };int main() {foo f;thread t1(&foo::bar1, &f, 5); //注意在調用非靜態類成員函數時,需要加上實例變量。t1.join();thread t2(&foo::bar2, 4);t2.join(); }

    9.2.4? 轉移線程的所有權

    thread是可移動的(movable)的,但不可復制的(copyable)。可以通過move來改變線程的所有權,靈活的決定線程在什么時候join或者detach。

    thread t1(f1); thread t3(move(t1));

    將線程從t1轉移給t3,這時候t1就不再擁有線程的所有權,調用t1.join或t1.detach會出現異常,要使用t3來管理線程。這也就意味著thread可以作為函數的返回類型,或者作為參數傳遞給函數,能夠更為方便的管理線程。

    9.2.5? 線程標識的獲取

    線程的標識類型為std::thread::id,有兩種方式獲得到線程的id:

  • 通過thread的實例調用get_id()直接獲取;
  • 在當前線程上調用this_thread::get_id()獲取。
  • 9.2.6? 線程暫停

    如果讓線程從外部暫停會引發很多并發問題,這也是為什么std::thread沒有直接提供pause函數的原因。如果線程在運行過程中,確實需要停頓,就可以用this_thread::sleep_for。

    void threadCaller() {this_thread::sleep_for(chrono::seconds(3)); //此處線程停頓3秒。cout<<"thread pause for 3 seconds"<<endl; }int main() {thread t(threadCaller);t.join(); }

    9.2.7? 異常情況下等待線程完成

    為了避免主線程出現異常時將子線程終結,就要保證子線程在函數退出前完成,即在函數退出前調用join()。

    方法一:異常捕獲

    void func() {thread t([]{cout << "hello C++ 11" << endl;});try{do_something_else();}catch (...){t.join();throw;}t.join(); }

    方法二:資源獲取即初始化(RAII)

    class thread_guard {private:thread &t;public:/*加入explicit防止隱式轉換,explicit僅可加在帶一個參數的構造方法上,如:Demo test; test = 12.2;這樣的調用就相當于把12.2隱式轉換為Demo類型,加入explicit就禁止了這種轉換。*/explicit thread_guard(thread& _t) {t = _t;}~thread_guard(){if (t.joinable())t.join();}thread_guard(const thread_guard&) = delete; //刪除默認拷貝構造函數thread_guard& operator=(const thread_guard&) = delete; //刪除默認賦值運算符 };void func(){thread t([]{cout << "Hello thread" <<endl ;});thread_guard guard(t); }

    無論是何種情況,當函數退出時,對象guard調用其析構函數銷毀,從而能夠保證join一定會被調用。

    9.3? 線程的同步與互斥

    線程之間通信的兩個基本問題是互斥和同步:

    • 線程同步是指線程之間所具有的一種制約關系,一個線程的執行依賴另一個線程的消息,當它沒有得到另一個線程的消息時應等待,直到消息到達時才被喚醒。
    • 線程互斥是指對于共享的操作系統資源,在各線程訪問時的排它性。當有若干個線程都要使用某一共享資源時,任何時刻最多只允許一個線程去使用,其它要使用該資源的線程必須等待,直到占用資源者釋放該資源。

    線程互斥是一種特殊的線程同步。實際上,同步和互斥對應著線程間通信發生的兩種情況:

    • 當一個線程需要將某個任務已經完成的情況通知另外一個或多個線程時;
    • 當有多個線程訪問共享資源而不使資源被破壞時。

    在WIN32中,同步機制主要有以下幾種:

  • 臨界區(Critical Section):通過對多線程的串行化來訪問公共資源或一段代碼,速度快,適合控制數據訪問。 ?
  • 事件(Event):用來通知線程有一些事件已發生,從而啟動后繼任務的開始。
  • 信號量(Semaphore):為控制一個具備有限數量用戶資源而設計。 ?
  • 互斥量(Mutex):為協調一起對一個共享資源的單獨訪問而設計的。 ??
  • 9.3.1? 臨界區

    臨界區(Critical Section)是一段獨占對某些共享資源訪問的代碼,在任意時刻只允許一個線程對共享資源進行訪問。如果有多個線程試圖同時訪問臨界區,那么在有一個線程進入后其他所有試圖訪問此臨界區的線程將被掛起,并一直持續到進入臨界區的線程離開。臨界區在被釋放后,其他線程可以繼續搶占,并以此達到用原子方式操作共享資源的目的。

    臨界區在使用時以CRITICAL_SECTION結構對象保護共享資源,并分別用EnterCriticalSection()和LeaveCriticalSection()函數去標識和釋放一個臨界區。所用到的CRITICAL_SECTION結構對象必須經過InitializeCriticalSection()的初始化后才能使用,而且必須確保所有線程中的任何試圖訪問此共享資源的代碼都處在此臨界區的保護之下。否則臨界區將不會起到應有的作用,共享資源依然有被破壞的可能。

    #include "stdafx.h" #include<windows.h> #include<iostream> using namespace std;int number = 1; //定義全局變量 CRITICAL_SECTION Critical; //定義臨界區句柄unsigned long __stdcall ThreadProc1(void* lp) {while (number < 100){EnterCriticalSection(&Critical);cout << "thread 1 :"<<number << endl;++number;_sleep(100);LeaveCriticalSection(&Critical);}return 0; }unsigned long __stdcall ThreadProc2(void* lp) {while (number < 100){EnterCriticalSection(&Critical);cout << "thread 2 :"<<number << endl;++number;_sleep(100);LeaveCriticalSection(&Critical);}return 0; }int main() {InitializeCriticalSection(&Critical); //初始化臨界區對象CreateThread(NULL, 0, ThreadProc1, NULL, 0, NULL);CreateThread(NULL, 0, ThreadProc2, NULL, 0, NULL);Sleep(10*1000);system("pause");return 0; }

    9.3.2? 事件

    事件對象能夠通過通知操作的方式來保持線程的同步,并且能夠實現不同進程中的線程同步操作。事件可以處于激發狀態(signaled or true)或未激發狀態(unsignal or false)。根據狀態變遷方式的不同,事件可分為兩類:

  • 手動設置:這種對象只可能用程序手動設置,在需要該事件或者事件發生時,采用SetEvent及ResetEvent來進行設置。
  • 自動恢復:一旦事件發生并被處理后,自動恢復到沒有事件狀態,不需要再次設置。
  • 使用”事件”機制應注意以下事項:

  • 如果跨進程訪問事件,必須對事件命名,在對事件命名的時候,要注意不要與系統命名空間中的其它全局命名對象沖突;
  • 事件是否要自動恢復;
  • 事件的初始狀態設置。
  • #include "stdafx.h" #include<windows.h> #include<iostream> using namespace std;int number = 1; //定義全局變量 HANDLE hEvent; //定義事件句柄unsigned long __stdcall ThreadProc1(void* lp) {while (number < 100){WaitForSingleObject(hEvent, INFINITE); //等待對象為有信號狀態cout << "thread 1 :"<<number << endl;++number;_sleep(100);SetEvent(hEvent);}return 0; }unsigned long __stdcall ThreadProc2(void* lp) {while (number < 100){WaitForSingleObject(hEvent, INFINITE); //等待對象為有信號狀態cout << "thread 2 :"<<number << endl;++number;_sleep(100);SetEvent(hEvent);}return 0; }int main() {CreateThread(NULL, 0, ThreadProc1, NULL, 0, NULL);CreateThread(NULL, 0, ThreadProc2, NULL, 0, NULL);hEvent = CreateEvent(NULL, FALSE, TRUE, "event");Sleep(10*1000);system("pause");return 0; }

    由于event對象屬于內核對象,故進程B可以調用OpenEvent函數通過對象的名字獲得進程A中event對象的句柄,然后將這個句柄用于ResetEvent、SetEvent和WaitForMultipleObjects等函數中。此法可以實現一個進程的線程控制另一進程中線程的運行,例如:

    HANDLE hEvent=OpenEvent(EVENT_ALL_ACCESS,true,"MyEvent"); ResetEvent(hEvent);

    9.3.3? 信號量

    信號量對象對線程的同步方式和前面幾種方法不同,信號允許多個線程同時使用共享資源,但是需要限制在同一時刻訪問此資源的最大線程數目。

    用CreateSemaphore()創建信號量時即要同時指出允許的最大資源計數和當前可用資源計數。一般是將當前可用資源計數配置為最大資源計數,每增加一個線程對共享資源的訪問,當前可用資源計數就會減1,只要當前可用資源計數是大于0的,就能夠發出信號量信號。但是當前可用計數減小到0時則說明當前占用資源的線程數已達到了所允許的最大數目,不能在允許其他線程的進入,此時的信號量信號將無法發出。線程在處理完共享資源后,應在離開的同時通過ReleaseSemaphore()函數將當前可用資源計數加1。在任何時候當前可用資源計數決不可能大于最大資源計數。?

    信號量包含的幾個操作原語: ??

    • CreateSemaphore() 創建一個信號量 ??
    • OpenSemaphore() 打開一個信號量 ??
    • ReleaseSemaphore() 釋放信號量 ??
    • WaitForSingleObject() 等待信號量 ?
    #include "stdafx.h" #include<windows.h> #include<iostream> using namespace std;int number = 1; //定義全局變量 HANDLE hSemaphore; //定義信號量句柄unsigned long __stdcall ThreadProc1(void* lp) {long count;while (number < 100){WaitForSingleObject(hSemaphore, INFINITE); //等待信號量為有信號狀態cout << "thread 1 :"<<number << endl;++number;_sleep(100);ReleaseSemaphore(hSemaphore, 1, &count);}return 0; }unsigned long __stdcall ThreadProc2(void* lp) {long count;while (number < 100){WaitForSingleObject(hSemaphore, INFINITE); //等待信號量為有信號狀態cout << "thread 2 :"<<number << endl;++number;_sleep(100);ReleaseSemaphore(hSemaphore, 1, &count);}return 0; }int main() {hSemaphore = CreateSemaphore(NULL, 1, 100, "sema");CreateThread(NULL, 0, ThreadProc1, NULL, 0, NULL);CreateThread(NULL, 0, ThreadProc2, NULL, 0, NULL);Sleep(10*1000);system("pause");return 0; }

    9.3.4? 互斥量

    采用互斥對象機制。 只有擁有互斥對象的線程才有訪問公共資源的權限,因為互斥對象只有一個,所以能保證公共資源不會同時被多個線程訪問?;コ獠粌H能實現同一應用程序的公共資源安全共享,還能實現不同應用程序的公共資源安全共享。

    互斥量包含的幾個操作原語: ??

    • CreateMutex() 創建一個互斥量 ??
    • OpenMutex() 打開一個互斥量 ??
    • ReleaseMutex() 釋放互斥量 ??
    • WaitForMultipleObjects() 等待互斥量對象 ?
    #include "stdafx.h" #include<windows.h> #include<iostream> using namespace std;int number = 1; //定義全局變量 HANDLE hMutex; //定義互斥對象句柄unsigned long __stdcall ThreadProc1(void* lp) {while (number < 100){WaitForSingleObject(hMutex, INFINITE);cout << "thread 1 :"<<number << endl;++number;_sleep(100);ReleaseMutex(hMutex);}return 0; }unsigned long __stdcall ThreadProc2(void* lp) {while (number < 100){WaitForSingleObject(hMutex, INFINITE);cout << "thread 2 :"<<number << endl;++number;_sleep(100);ReleaseMutex(hMutex);}return 0; }int main() {hMutex = CreateMutex(NULL, false, "mutex"); //創建互斥對象CreateThread(NULL, 0, ThreadProc1, NULL, 0, NULL);CreateThread(NULL, 0, ThreadProc2, NULL, 0, NULL);Sleep(10*1000);system("pause");return 0; }

    9.4? C++中的幾種鎖

    在9.3.4中我們講到了互斥量,其中CreateMutex等是Win32 api函數,而本節要介紹的std :: mutex來自C++標準庫。

    在C++11中線程之間的鎖有:互斥鎖、條件鎖、自旋鎖、讀寫鎖、遞歸鎖

    9.4.1? 互斥鎖

    互斥鎖是一種簡單的加鎖的方法來控制對共享資源的訪問。

    通過std::mutex可以方便的對臨界區域加鎖,std::mutex類定義于mutex頭文件,是用于保護共享數據避免從多個線程同時訪問的同步原語,它提供了lock、try_lock、unlock等幾個接口。使用方法如下:

    std::mutex mtx; mtx.lock() do_something...; //共享的數據 mtx.unlock();

    mutex的lock和unlock必須成對調用,lock之后忘記調用unlock將是非常嚴重的錯誤,再次lock時會造成死鎖。

    此時可以使用類模板std::lock_guard,通過RAII機制在其作用域內占有mutex,當程序流程離開創建lock_guard對象的作用域時,lock_guard對象被自動銷毀并釋放mutex。lock_guard構造時還可以傳入一個參數adopt_lock或者defer_lock。adopt_lock表示是一個已經鎖上了鎖,defer_lock表示之后會上鎖的鎖。

    std::mutex mtx; std::lock_guard<std::mutex> guard(mtx); do_something...; //共享的數據

    lock_guard類最大的缺點也是簡單,沒有給程序員提供足夠的靈活度,因此C++11定義了另一個unique_guard類。這個類和lock_guard類似,也很方便線程對互斥量上鎖,但它提供了更好的上鎖和解鎖控制,允許延遲鎖定、鎖定的有時限嘗試、遞歸鎖定、所有權轉移和與條件變量一同使用。

    #include <iostream> // std::cout #include <thread> // std::thread #include <mutex> // std::mutex, std::unique_lock #include <vector>std::mutex mtx; // mutex for critical section std::once_flag flag;void print_block (int n, char c) {//unique_lock有多組構造函數, 這里std::defer_lock不設置鎖狀態std::unique_lock<std::mutex> my_lock (mtx, std::defer_lock);//嘗試加鎖, 如果加鎖成功則執行//(適合定時執行一個job的場景, 一個線程執行就可以, 可以用更新時間戳輔助)if(my_lock.try_lock()){for (int i=0; i<n; ++i)std::cout << c;std::cout << '\n';} }void run_one(int &n){std::call_once(flag, [&n]{n=n+1;}); //只執行一次, 適合延遲加載; 多線程static變量情況 }int main () {std::vector<std::thread> ver;int num = 0;for (auto i = 0; i < 10; ++i){ver.emplace_back(print_block,50,'*');ver.emplace_back(run_one, std::ref(num));}for (auto &t : ver){t.join();}std::cout << num << std::endl;return 0; }

    unique_lock比lock_guard使用更加靈活,功能更加強大,但使用unique_lock需要付出更多的時間、性能成本。

    9.4.2? 條件鎖

    條件鎖就是所謂的條件變量,當某一線程滿足某個條件時,可以使用條件變量令該程序處于阻塞狀態;一旦該條件狀態發生變化,就以“信號量”的方式喚醒一個因為該條件而被阻塞的線程。

    最為常見就是在線程池中,起初沒有任務時任務隊列為空,此時線程池中的線程因為“任務隊列為空”這個條件處于阻塞狀態。一旦有任務進來,就會以信號量的方式喚醒一個線程來處理這個任務。

    • 頭文件:<condition_variable>
    • 類型:std::condition_variable(只與std::mutex一起工作)、std::condition_variable_any(可與符合類似互斥元的最低標準的任何東西一起工作)。
    std::deque<int> q; std::mutex mu; std::condition_variable cond;void function_1() //生產者 {int count = 10;while (count > 0) {std::unique_lock<std::mutex> locker(mu);q.push_front(count);locker.unlock();cond.notify_one(); // Notify one waiting thread, if there is one.std::this_thread::sleep_for(std::chrono::seconds(1));count--;} }void function_2() //消費者 {int data = 0;while (data != 1) {std::unique_lock<std::mutex> locker(mu);while (q.empty())cond.wait(locker); // Unlock mu and wait to be notifieddata = q.back();q.pop_back();locker.unlock();std::cout << "t2 got a value from t1: " << data << std::endl;} } int main() {std::thread t1(function_1);std::thread t2(function_2);t1.join();t2.join();return 0; }

    上面是一個生產者-消費者模型,軟件開啟后,消費者線程進入循環,在循環里獲取鎖,如果消費品隊列為空則wait,wait會自動釋放鎖;此時消費者已經沒有鎖了,在生產者線程里,獲取鎖,然后往消費品隊列生產產品,釋放鎖,然后notify告知消費者退出wait,消費者重新獲取鎖,然后從隊列里取消費品。

    9.4.3? 自旋鎖

    當發生阻塞時,互斥鎖會讓CPU去處理其他的任務,而自旋鎖則會讓CPU一直不斷循環請求獲取這個鎖。由此可見“自旋鎖”是比較耗費CPU的。在C++中我們可以通過原子操作實現自旋鎖:

    //使用std::atomic_flag的自旋鎖互斥實現 class spinlock_mutex{ private:std::atomic_flag flag; public:spinlock_mutex():flag(ATOMIC_FLAG_INIT) {}void lock(){while(flag.test_and_set(std::memory_order_acquire));}void unlock(){flag.clear(std::memory_order_release);} }

    9.4.4? 讀寫鎖

    說到讀寫鎖我們可以借助于“讀者-寫者”問題進行理解。

    計算機中某些數據被多個進程共享,對數據庫的操作有兩種:一種是讀操作,就是從數據庫中讀取數據不會修改數據庫中內容;另一種就是寫操作,寫操作會修改數據庫中存放的數據。因此可以得到我們允許在數據庫上同時執行多個“讀”操作,但是某一時刻只能在數據庫上有一個“寫”操作來更新數據。這就是一個簡單的讀者-寫者模型。

    頭文件:boost/thread/shared_mutex.cpp
    類型:boost::shared_lock、boost::shared_mutex

    shared_mutex比一般的mutex多了函數lock_shared() / unlock_shared(),允許多個(讀者)線程同時加鎖和解鎖;而shared_lock則相當于共享版的lock_guard。對于shared_mutex使用lock_guard或unique_lock就可以達到寫者線程獨占鎖的目的。

    讀寫鎖的特點:

  • 如果一個線程用讀鎖鎖定了臨界區,那么其他線程也可以用讀鎖來進入臨界區,這樣可以有多個線程并行操作。這個時候如果再用寫鎖加鎖就會發生阻塞。寫鎖請求阻塞后,后面繼續有讀鎖來請求時,這些后來的讀鎖都將會被阻塞。這樣避免讀鎖長期占有資源,防止寫鎖饑餓。
  • 如果一個線程用寫鎖鎖住了臨界區,那么其他線程無論是讀鎖還是寫鎖都會發生阻塞。
  • 9.4.5? 遞歸鎖

    遞歸鎖又稱可重入鎖,在同一個線程在不解鎖的情況下,可以多次獲取鎖定同一個遞歸鎖,而且不會產生死鎖。遞歸鎖用起來固然簡單,但往往會隱藏某些代碼問題。比如調用函數和被調用函數以為自己拿到了鎖,都在修改同一個對象,這時就很容易出現問題。

    9.5? C++中的原子操作

    9.5.1??atomic模版函數

    為了避免多個線程同時修改全局變量,C++11除了提供互斥量mutex這種方法以外,還提供了atomic模版函數。使用atomic可以避免使用鎖,而且更加底層,比mutex效率更高。

    #include <thread> #include <iostream> #include <vector> #include <atomic>using namespace std;void func(int& counter) {for (int i = 0; i < 100000; ++i){++counter;} }int main() {//atomic<int> counter(0);atomic_int counter(0); //新建一個整型原子counter,將counter初始化為0//int counter = 0;vector<thread> threads;for (int i = 0; i < 10; ++i){threads.push_back(thread(func, ref(counter)));}for (auto& current_thread : threads){current_thread.join();}cout << "Result = " << counter << '\n';return 0; }

    為了避免多個線程同時修改了counter這個數導致出現錯誤,只需要把counter的原來的int型,改為atomic_int型就可以了,非常方便,也不需要用到鎖。

    9.5.2??std::atomic_flag

    std::atomic_flag是一個原子型的布爾變量,只有兩個操作:

    1)test_and_set,如果atomic_flag 對象已經被設置了,就返回True,如果未被設置,就設置之然后返回False

    2)clear,把atomic_flag對象清掉

    注意這個所謂atomic_flag對象其實就是當前的線程。如果當前的線程被設置成原子型,那么等價于上鎖的操作,對變量擁有唯一的修改權。調用clear就是類似于解鎖。

    下面先看一個簡單的例子,main() 函數中創建了 10 個線程進行計數,率先完成計數任務的線程輸出自己的 ID,后續完成計數任務的線程不會輸出自身 ID:

    #include <iostream> // std::cout #include <atomic> // std::atomic, std::atomic_flag, ATOMIC_FLAG_INIT #include <thread> // std::thread, std::this_thread::yield #include <vector> // std::vectorstd::atomic<bool> ready(false); // can be checked without being set std::atomic_flag winner = ATOMIC_FLAG_INIT; // always set when checkedvoid count1m(int id) {while (!ready) {std::this_thread::yield();} // 等待主線程中設置 ready 為 true.for (int i = 0; i < 1000000; ++i) {} // 計數.// 如果某個線程率先執行完上面的計數過程,則輸出自己的 ID.// 此后其他線程執行 test_and_set 是 if 語句判斷為 false,// 因此不會輸出自身 ID.if (!winner.test_and_set()) {std::cout << "thread #" << id << " won!\n";} };int main() {std::vector<std::thread> threads;std::cout << "spawning 10 threads that count to 1 million...\n";for (int i = 1; i <= 10; ++i)threads.push_back(std::thread(count1m, i));ready = true;for (auto & th:threads)th.join();return 0; }

    再來一個例子:

    #include <iostream> #include <atomic> #include <vector> #include <thread> #include <sstream>std::atomic_flag lock = ATOMIC_FLAG_INIT; //初始化原子flag std::stringstream stream;void append_number(int x) {while(lock.test_and_set()); //如果原子flag未設置,那么返回False,就繼續后面的代碼。否則一直返回True,就一直停留在這個循環。stream<<"thread#" <<x<<'\n';lock.clear(); //去除flag的對象 }int main() {std::vector<std::thread> threads;for(int i=0;i<10;i++)threads.push_back(std::thread(append_number, i));for(auto& th:threads)th.join();std::cout<<stream.str()<<'\n'; }

    9.6??相關面試題

    Q:C++怎么保證線程安全

    A:

    Q:悲觀鎖和樂觀鎖

    A:悲觀鎖:悲觀鎖是就是悲觀思想,即認為讀少寫多,遇到并發寫的可能性高,每次去拿數據的時候都認為別人會修改,所以每次在讀寫數據的時候都會上鎖,這樣別人想讀寫這個數據就會 block 直到拿到鎖。

    樂觀鎖:樂觀鎖是一種樂觀思想,即認為讀多寫少,遇到并發寫的可能性低,每次去拿數據的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,采取在寫時先讀出當前版本號,然后加鎖操作(比較跟上一次的版本號,如果一樣則更新),如果失敗則要重復【讀 - 比較 - 寫】的操作。

    Q:什么是死鎖

    A:所謂死鎖是指多個線程因競爭資源而造成的一種僵局(互相等待),若無外力作用,這些進程都將無法向前推進。

    Q:死鎖形成的必要條件

    A:

    產生死鎖必須同時滿足以下四個條件,只要其中任一條件不成立,死鎖就不會發生

    • 互斥條件:進程要求對所分配的資源(如打印機)進行排他性控制,即在一段時間內某 資源僅為一個進程所占有。此時若有其他進程請求該資源,則請求進程只能等待。
    • 不剝奪條件:進程所獲得的資源在未使用完畢之前,不能被其他進程強行奪走,即只能 由獲得該資源的進程自己來釋放(只能是主動釋放)。
    • 請求和保持條件:進程已經保持了至少一個資源,但又提出了新的資源請求,而該資源 已被其他進程占有,此時請求進程被阻塞,但對自己已獲得的資源保持不放。
    • 循環等待條件:存在一種進程資源的循環等待鏈,鏈中每一個進程已獲得的資源同時被 鏈中下一個進程所請求。即存在一個處于等待狀態的進程集合 {Pl, P2, …, pn},其中 Pi 等 待的資源被 P (i+1) 占有(i=0, 1, …, n-1),Pn 等待的資源被 P0 占有

    Q:什么是活鎖

    A:活鎖和死鎖在表現上是一樣的兩個線程都沒有任何進展,但是區別在于:死鎖,兩個線程都處于阻塞狀態而活鎖并不會阻塞,而是一直嘗試去獲取需要的鎖,不斷的 try,這種情況下線程并沒有阻塞所以是活的狀態,我們查看線程的狀態也會發現線程是正常的,但重要的是整個程序卻不能繼續執行了,一直在做無用功。

    Q:公平鎖與非公平鎖

    A:公平鎖:是指多個線程在等待同一個鎖時,必須按照申請鎖的先后順序來一次獲得鎖。

    非公平鎖:理解了公平鎖,非公平鎖就很好理解了,它無非就是不用排隊,當餐廳里的人出來后將鑰匙往地上一扔,誰搶到算誰的。

    ?

    ?

    總結

    以上是生活随笔為你收集整理的史上最全的C++面试宝典(合集)的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。