目錄
- 1 C++新語法
- C與C++
- C++編譯運行
- String
- 編程范式
- C++基礎類型
- **自動類型推導**
- 統一對象初始化:Uniform Initialization
- 控制結構
- if語句
- for語句
- switch語句
- namespace
- 2 函數
- 函數聲明
- 形式參數
- 函數參數傳遞的選擇
- 函數返回值的選擇
- 函數重載
- Lambda表達式
- 函數的定義和申明
- 生存期
- 鏈接性
- 3 STL
- 1.容器
- vector
- list
- set
- map
- stack
- queue
- 容器的共性
- 容器的選擇
- 2.迭代器
- 3.算法
- 4 類
- 類=屬性+方法
- 構造函數
- `explicit` 關鍵字
- 實例:復數類
- 類的內存管理
- 函數
- 數組
- 函數原型
- 處理文件
- 動態分配
- 抽象數據類型
- 正規函數
- Nice類
- 淺拷貝和深拷貝
- 構造vec模板類
- 類的數值特征
- 類型轉換
- 自動轉換
- 強制類型轉換
- 輸入輸出操作
- 兩元運算符
- 5 面向對象
- 繼承
- 抽象基類
- 繼承的可見性
- 繼承的類型
- 向上類型轉換
- 綁定
- 多態
- 切片
- 函數重載
- 組合
- 類的靜態成員變量
- 例子:單例模式
- 補充資料
- 異常處理
- try_catch
- noexcept
- noexcept(false)
- 期末考試試卷
1 C++新語法
C與C++
- 不同的標準庫函數
- 不同的編譯命令(g++,gcc)
- 指定語言標準(g++ -std=c++20)
C++編譯運行
編譯:
- 預處理
- #include指令,把相應的頭文件插入;
- #define 宏定義,直接對相應的內容進行替換;
- #ifdef #else 根據條件,包含(去除)部分代碼;
- 命令:
g++ -std=c++17 –E -P function.cpp –o function.i
- 文法分析
- 匯編
g++ -S -O1 function.i -o function.s
- 函數名稱特殊定義,全局變量名稱保留,函數和數據放在不同的部分
- 優化
- 代碼生成
g++ -c function.s -o function.o
- 使用objdump,readelf查看其內容
objdump –d function.o
鏈接:
- 命令:
g++ function.o clrmain.o –o main.exe
運行和動態鏈接:
- 代碼公用的部分放在共享動態庫中(windows中的dll或linux下的so文件),可減少.exe的文件大小,稱為動態鏈接。
- 通過PATH設置的環境變量去尋找目錄
String
std::string s; //未初始化的字符串內容為`""`
std::string t=s;
auto str=std::string(n,c); //用n個字符c來初始化z
const string spaces(greeting.size(), ' ');
編程范式
在命令式程序設計中,控制流程是顯式的,程序中的每一條語句表示了一條命令;而其先后關系則指明了執行的順序關系。
結構化程序設計進行了增強;構成部分包括:包含順序、選擇、循環以及遞歸四種形式的控制結構
過程化程序設計強調程序的模塊化,有時也把過程化程序設計稱為模塊化程序設計。在不同的語言中,模塊的定義是不一樣的:例如函數、包、類、對象等。
面向對象程序設計認為現實世界中的問題,實際上是現實世界中的實體(即對象)的相互作用產生的結果。面向對象程序設計涉及到類、類的對象、方法、消息。
在申明式程序設計中,控制流是隱式的但是程序員需要說明最終的結果。沒有賦值語句、沒有循環,只有SELECT、FROM、WHERE和ORDER等特定的子句(SQL)
函數式程序設計
C++基礎類型
C++實體(Entity):不包括宏
整數:
- 后綴是可選的,包括:long(L,l), long-long(LL, ll) unsigned(U, u)
- 可以表示10進制,8進制(前綴為0),16進制(前綴為0x或0X),二進制(前綴為0b或0B)
浮點數:
- 后綴是可選的,包括:float(f, F), double(無后綴), long double(l, L)
nullptr
- C++中NULL被定義為0;
提示:可以使用is_xxx的函數判斷給定的類型是否符合條件,需要#include <type_traits>
基本類型: std::is_fundamental
Void
: std::is_voidnullptr_t
: std::is_null_pointer- 整數類型 :std::is_integral,包含布爾、字符、整數類型
- 浮點類型 : std::is_floating_point
- 其中整數類型和浮點類型又稱為數值類型: std::is_arithmetic
復合類型:std::is_compound
- 引用類型: std::is_reference
- 指針類型: std::is_pointer
- 成員指針類型: std::is_member_pointer
- 數組類型: std::is_array
- 函數類型: std::is_function
- 枚舉類型: std::is_enum
- 類類型: 非union類型std::is_class
- 類類型: union類型 std::is_union
其他類型
- 對象類型(std::is_object):除函數、引用或void類型以外的所有類型
- 標量類型(std::is_scalar):數值類型、枚舉類型、指針類型、成員指針類型、nullptr_t
- 對非引用和非函數類型,類型系統支持三種cv限定符,即const、volatitle和const volatile.
typedef和using只是類型的別名
typedef unsigned long ulong;
using ulong = unsigned;
自動類型推導
類型后綴
L:long
LL:long long
U:unsigned
f:float(默認為double)
l:long double
auto(C11及以后)
例子:
- 浮點默認為double
- 字符串默認為const char*
注意:
- 可以使用const,*,&,&&來修飾auto
- auto必須有初始化的值
- 如果使用auto定義多個變量,其推導類型必須一致(如
auto i,j,k
) - auto推導會自動去除const或volatile修飾符
decltype
等價于declare type
decltype
是C++中的一個關鍵字,用于在編譯時確定表達式的類型。它允許您在不實際評估表達式的情況下提取其類型
decltype(表達式)
可以對實體或者表達式進行類型推導。且與auto不一樣的是,decltype推導出的類型與實體或表達式的值類型(value category)相關。
-
如果e值類型是xvalue(將亡值), 那么decltype(e)類型為T&&
-
如果e值類型是lvalue(左值), 那么decltype(e)類型為T&
-
如果e值類型是prvalue(純右值), 那么decltype(e)類型為T
decltype與auto不一樣:decltype保留const和volatile
const int ci = 34;
decltype(ci) cj = ci * 2;
decltype(i+ci) ri = i+ci;
decltype(T().begin()) m_it
統一對象初始化:Uniform Initialization
在傳統C++代碼中,各種對象的初始化差異很大,請看下面的代碼
在現代C++代碼中,統一使用{}進行初始化
局限:不能進行數據的轉換,即發生類型的narrowing時,統一初始化會出錯
error: narrowing conversion of ‘4.5e+0’ from ‘double’ to ‘int’ [-Wnarrowing]
控制結構
if語句
帶變量初始化的if語句
帶constexpr
的if語句
- 當if語句中存在constexpr時,if語句具有編譯時執行的特性:即該if語句在編譯時執行,如果條件為真,那么假分支將被拋棄;否則真分支將被拋棄。
for語句
支持range-based for語句
for(auto& e:edges){e.print();
}
對于復雜對象,如果不需要修改,則使用const auto&;否則使用auto &;
for(const auto& student: students){if (student.name == name)return true;
}
C++支持結構化綁定(structure binding),使得代碼編寫更簡潔
for(const auto& [id, sname, address]: students){if (sname == name)return true;
}
零成本抽象(zero overhead abstraction)–好的語言特性不會導致運行代價;也不要付出代價。
switch語句
不寫break;語句會導致fall back through;
強類型枚舉:enum class RGB {…}
namespace
名字空間是C++區別C的一個重要方面。
- 名字空間可以避免名字沖突;同時可以把代碼組織成模塊;
#include <iostream>
namespace fun {int GetMeaningOfLife(void){return 8;}
};
namespace boring {int GetMeaningOfLife(void){return 5;}
};
int main() {std::cout << boring::GetMeaningOfLife() << '\n'<< fun::GetMeaningOfLife() << '\n';
}
可以方便地使用自定義的類型。
#include "vec.h"
#include <vector>
int main(){std::vector<int> v1;vec::vector<int> v2; //使用自定義的vector類型
}
建議:不要在頭文件中使用 using namespace …語句;
- 會污染全局命名空間,可能導致意外的名稱沖突。
2 函數
函數聲明
形式參數
-
函數外部
-
傳值
-
傳地址
-
-
函數內部
-
傳指針
-
傳引用
-
傳值方式:首先計算實參的值,然后把實參的值拷貝給形參
傳地址方式:實參的地址直接傳給形參,在函數內通過地址訪問原對象
傳指針方式
T* const a; 和 const T* a; T const* a;這三者分別代表什么?
答:1是常量指針,2,3都是指向常量的指針;
const T& 和 T const&是一樣的;但是 T& const會報錯?
答:引用不是對象,不可被定義為常量
指針問題:
野指針
- 內存的分配與釋放必須匹配,如果部匹配就會形成野指針
野指針產生的原因
- 指針定義時沒有初始化:指向隨機的內存空間;
- 指針釋放后沒有置空:free§,只是釋放內存,并不把p設置為空指針;
- 指針超越變量作用域:局部變量在函數調用后被釋放,因此返回函數內的局部變量會產生也指針。
C++中不要使用指針!
- C++中有智能指針作為替代,基本上解決了這些問題!
傳引用方式
引用類型(左值引用)
- 任意類型T,其左值引用類型為T&,即在類型后面附加一個&號。
- 左值引用在定義時必須指定初始值,且一旦引用創建之后不可更改:左值引用具有常量的含義。
- 引用不用釋放
- 注意:const T& 和 T const&是一樣的;但是 T& const會報錯。
注意:
- 函數內部的引用和局部變量一樣,具有塊作用域,在塊結束時,編譯器自動銷毀引用變量;
- 非函數內部的引用和全局變量一樣,其生存期是整個程序運行的周期。不建議使用全局引用。
函數參數傳遞的選擇
設計函數,在考慮函數形參類型時,遵循以下規則
- 沒有特殊要求的情況下,一概不用指針形參
- 對于數值類型使用傳值方式;除非要求修改實參的值,此時使用傳引用(T&)方式;
- 對于結構類型、類類型或其他復雜的數據結構,如果無需修改實參,使用const T&類型;如果需要修改實參,使用T&類型。
函數返回值的選擇
- 不要使用指針
- 盡量使用T類型作為返回值
- 盡量不要使用引用類型的返回值
函數重載
- 名稱相同,但是形參長度或者類型排列不一樣的函數
技術手段:名字修飾和重載決議
- 名字修飾:C++編譯器在編譯階段把函數名處理成特定的名稱:名字前綴和參數編碼
- 名字前綴:處理成 _Z8exchange
- 參數編碼:基本數據類型有單獨的表示;用P代表指針類型、R代表左值類型、O代表右值引用類型,k代表常類型;S_代表當前形參類型與前一形參類型相同(僅在前一類型的描述長度小于2時使用)
- 重載決議(overload resolution)
- 在編譯時,對每一個函數調用,執行重載決議;
- 對每一個函數調用,推測其函數實參的類型,找出所有可能的候選函數集合;
- 如果當前候選函數集合僅有一個函數的實現,則選擇該函數;否則重載失敗:有多個滿足條件的函數時,編譯器無法決定具體采用哪一種(二義性)。
- 如果定義了很多重載函數而不調用,就不會執行重載決議:盡管某些函數調用會出現代碼有問題的情況!
Lambda表達式
// chapt04/lambda_hello.cpp
#include <iostream>
int main(){[] (auto msg){ std::cout << msg ; }("Hello world!\n"); //同時實現函數定義和調用
}
默認捕獲
- [=] 默認捕獲方式為傳值捕獲
- [&]默認捕獲方式為引用捕獲
捕獲方式可以混合
- [value, &count]
- [[value, this]
- [[&value, &count, &this]
可變規則語法為:Mutable
-
可變規則只影響通過傳值方式捕獲的變量;
-
如果捕獲列表中沒有任何傳值捕獲的對象,那么mutable沒有作用;
-
如果捕獲列表中有任何傳值捕獲的對象,則mutable運行這些傳值捕獲的對象可以在函數體內進行修改;但是這種修改不會改變其值。
#容器內元素排序
using Vector = vector<int>;
Vector sortValues(){Vector values = {1,2,3,4,5,6,7,8,9};sort(values.begin(), values.end(), [](int left, int right){return (left+1) % 3 < (right+1) % 3;});return vlaues;
}
#查找容器內符合條件的元素
void findValues()
{Vector values = {1, 2, 3, 4, 5, 6, 7, 8, 9};int target = 8;auto pos = find_if(values.begin(), values.end(),[target](int value) //target被捕獲{return value == target;});if (pos != values.end())cout << "Found\n";
}
函數的定義和申明
定義:完全定義清楚了實體的申明(賦了初值的一定是定義)
什么是申明?
-
沒有函數體的函數申明
- int fun(int);
-
申明前帶有extern或extern “C”存儲標識且沒有初始化語句的;
-
extern const int a; 非定義
-
extern const int b=1; 定義
-
-
前向申明(不完全類型申明):struct S;
-
類型別名:typedef類型申明;using類型別名;using申明
變量的定義
- int value;
- int value = 10;
- extern int value = 10;
變量的申明
- extern int value;
ODR原則規定申明可以多次出現,但是定義只能有一次
如果一個對象、引用或函數被 ODR 使用,則其定義必須存在于程序中的某處;否則常為連接時錯誤。
生存期
靜態生存期
- 全局變量
自動生存期
- 局部變量或函數形參
動態生存期
- 使用new或malloc分配的內存,用delete或free進行釋放
鏈接性
無鏈接
- 局部變量;內部類及成員函數;申明于塊內部的類型或變量;
內部鏈接
- 申明為static的變量、函數或函數模板;
外部鏈接
- 全局變量、非靜態的函數。
3 STL
1.容器
所有STL容器提供基于值的語義而非基于引用的語義:容器管理的是對象,而非指針或引用;
STL容器分類
- 序列容器:array,
vector
, deque,list
, forward_list - 關聯容器:
set
,map
, multiset, multimap - 無序關聯容器:unorder_* (set, map, multiset, multimap)
- 適配器容器:stack, queue, priority_queue
vector
- 支持隨機訪問(operate[]操作符)
- array:固定大小的空間
方法:
- 支持array的所有操作
- clear-清除所有元素
- reserve-預留空間
- resize-更改存儲元素的格式
- insert,erase
- push_back,pop_back
缺點:
- 從中間插入/刪除會移動所有元素
- vector預留的空間不足時,插入元素導致分配新空間以及拷貝所有元素(建議使用
reverse
函數預留空間)
適用對象簡單,變化較小,并且頻繁隨機訪問的場景
list
雙向鏈表
方法:
- vector的所有操作,除了at()和operator[]
- push_front:添加到頭部
- pop_front:刪除頭部
優點:
- 插入和刪除為O(1)
缺點:
- 不支持元素隨機訪問:訪問頭尾O(1),訪問中間元素為O(N)
- 不能提前預留空間
set
集合,不允許重復元素(multiset
允許),屬于關聯容器
對于set/multiset必須定義<(小于操作符),用于比較存放順序
方法:
- clear
- insert
- erase
- find-搜索元素,返回迭代器
- contains(c++20)-檢查元素是否存在于集合中
優點:
- 元素自動排序,且沒有重復元素
- 查找效率log2N(使用紅黑樹實現)
缺點:
- 插入刪除效率一般,log2N
適用于需要經常查找元素是否存在,并且需要排序
map
映射,含有Key-value對的已排序映射,要求Key是唯一的(multimap
允許多個相同的鍵存在)
屬于關聯容器
對于map/multimap必須定義<操作符:需要使用“小于”關系定義順序
方法:
- clear
- insert
- erase
- find-返回迭代器
- contains(c++20)
- merge(c++20)-把另一個map的鍵值對加到當前容器
優點:
- 使用紅黑樹實現,查找效率為log2N
缺點:
- 插入刪除需要調整紅黑樹,log2N
適用于
- 需要存儲一對(key-value)數據
- 要求根據key找value的情景
- 典型應用:單詞統計、建立交叉引用列表
關聯容器的共同點
包括set/multiset、map/multimap
- 關聯容器都需要排序:需要在元素上定義<操作符。同時在插入、刪除時,根據該操作符對元素進行排序
- 管理容器的實現以紅黑樹為基礎,因此其查找效率為O(log2N);于此同時,序列容器的查找效率為O(N)
- 關聯式容器在每次插入、刪除元素時,都必須按照排序規則重新調整紅黑樹;但是序列容器在每次插入、刪除容器中的元素時,往往并不做排序,通常在所有的插入或刪除之后進行一次sort,而這種方式往往性能更優
無序關聯容器
包括unordered_set/unordered_multiset、unordered_map/unordered_multimap
- 無序關聯容器中的元素都是無序的:取消了關聯容器中key必須排序的要求
- 無序關聯容器的底層實現采用了哈希表的形式,因此需要指定h哈希函數;此外,還要定義=操作符,以確定兩個元素是否相等
- 在無序關聯容器中,如果不考慮計算key的哈希值的時間,那么查找指定元素的時間復雜度為O(1),遠比關聯容器的O(log2N)的查找要快
方法:
- 迭代器-只支持begin/end
- 容量-empty,size,max_size,reverse
- 增刪-insert,erase,clear,swap,merge,
- extract-提取指定鍵的迭代器
- 查詢-find,contains
- count-返回鍵的數量
容器適配器
包括stack, queue, 以及priority_queue三大類
借助底層容器(deque或vector)實現,經過封裝
stack
后進先出
底層容器默認是deque,需要支持back,push_back,pop_back
方法:
- push
- pop
- top-訪問棧頂元素
- empty
queue
先進先出
底層容器默認是list(不能是vector),需要支持back,front,push_back,pop_front
方法:
- push-向隊尾壓入元素
- pop-從隊首取出元素
- back
- front
- empty
容器的共性
- 所有容器提供值語義,而非引用語義。這意味著任何一種類型的容器,都可以像操作整數類型一樣,簡潔而沒有后遺癥;
- 元素在容器內部有特定的順序。同時每種容器都提供迭代器
- 通過for-each遍歷容器
- 使用同類型的容器,需要的代碼修改可能很少
容器的選擇
- 對于元素個數固定的容器,選擇array;
- 對于元素個數不固定的容器,選擇vector。Vector的內容不結構最簡單,支持隨機訪問,十分靈活;
- 如果經常需要在頭、尾插入或移除操作,應該使用deque;如果希望元素被刪除時,容器能自動縮減內部使用的內存,也應該采用deque;
- 如果經常需要在容器中執行元素的插入、移動和刪除,應該使用list;list在頭、尾進行插入或移除,效率高;但是list不支持隨機訪問;
2.迭代器
該模式提供一種方法,在不暴露對象內部表示的情況下,順序訪問對象中的每一個元素。
迭代器分為5類,分別是
- 輸入迭代器: istream
- 輸出迭代器: ostream
- 前向迭代器: forward_list, unordered_容器
- 雙向迭代器: list, (multi)set, (multi)map
- 隨機迭代器: array, vector, deque
萬能膠:能夠使得容器與算法互不干擾獨立發展,最后又能無縫的粘合起來。
逆向迭代器:[ container.rbegin(), container.rend() )這樣的半閉半開區間表示
插入迭代器包括:前插迭代器、后插迭代器,以及插入迭代器
// 使用range-based for
for(auto value: a) b.push_back(value);// 使用copy函數,并使用便捷函數
copy(a.begin(), a.end(), back_inserter(b));
流迭代器
//從標準輸出流構造一個輸出流迭代器
std::ostream_iterator out_iter {std::cout, " "};
//從標準輸入流構造一個輸入流迭代器
std::istream_iterator<string> in_iter {std::cin};
std::istream_iterator<string> in_end_iter;
//從標準輸入流中讀取浮點值,并用它們作為容器中元素的初始
std::vector<double> data;
std::copy(std::istream_iterator<double>{std::cin}, std::istream iterator<double>{}, std::back_inserter(data));
3.算法
算法部分主要頭文件包括: algorithm、numeric 和 functional
查找算法——版本4
template <typename Iterator, typename T>
Iterator search(Iterator begin, Iterator end, const T &value)
{while (begin != end && *begin != value)++begin;return begin;
}
模板函數
- 代表一類同構函數,比如search函數,可以針對int\long\float等可以比較的類型。
- 使用關鍵字template;<>包含模板參數:分為類型模板參數;非類型模板參數;以及模板模板參數。
模板類
- 代表一類同構的類,例如vector,不管里面的元素是哪一種類型,都是用vector表示;
- 使用關鍵字template;<>包含模板參數:分為類型模板參數;非類型模板參數;以及模板模板參數。
template<class T,class Allocator = std::allocator<T>>
class vector;
4 類
類=屬性+方法
通過在參數列表后插入const關鍵字,可以把成員函數定義為const函數
- 這樣的成員函數不能改變調用它們的對象的狀態
- const對象只能調用const成員函數
成員函數與非成員函數
三種保護標簽:
- struct默認是公有保護
- class默認是私有保護
訪問器函數(getter):允許訪問私有數據
std::string getname() const{return name;}
函數的申明和定義
- 申明:只明確了接口,含名字、參數及返回值。
- 定義:完整地描述函數的功能,說明了每一步的功能。
class Student_info{
public:double grade() const;std::istream & read(std::istream &);//申明std::string getname() const{return name;}//定義
在函數前加上關鍵字inline —— 內聯函數
- 好處:編譯器將函數調用改成函數本體,避免了函數調用開銷
檢測成員函數是否為空
bool valid()const {return homework.empty();}
class MyClass {
private:MyClass() { /* 構造函數實現 */ }public:static MyClass createInstance() {return MyClass();}
};
構造函數
可以顯式的調用類的構造函數;創建類的一個對象時,會自動調用適當的構造函數;
如果類中沒有定義任何構造函數,那么編譯器自動合成一個構造函數。
構造函數重載:一個類可以定義多個構造函數,只要參數個數或者類型不同,確切地說,只要函數的簽名不一樣就可以。
編程習慣:從任何一個構造函數退出前,確保每個數據成員都有一個有意義的值。
默認構造函數:1)不帶有任何參數的構造函數;或者2)所有參數都是默認值參數的構造函數;
Student_info::Student_info() : midterm{0}, final{0} {}
explicit
關鍵字
防止隱式類型轉換
class MyClass {
public:explicit MyClass(int x) {// 構造函數的實現}
};
int main() {MyClass obj = 10; // 錯誤: 無法從 'int' 轉換為 'MyClass'MyClass obj2(10); // 正確return 0;
}
實例:復數類
class Complex
{
private:double real_{}, imag_{};
public:double& real() { return real_; }const double& real() const { return real_; }double& imag() { return imag_; }const double& imag() const { return imag_; }
public:Complex() : Complex(0.f, 0.f) {}Complex(double real) : Complex(real, 0.f) {}Complex(double real, double imag) :_real{real}, imag_{imag} { }Complex(const Complex& th): real_{th._real}, imag_{th._imag} {}~Complex(){ }
public:Complex& operator+=(const double& __t);Complex& operator+=(const Complex& __z);friend std::istream& operator>>(std::istream& is, Complex& rhs);friend std::ostream& operator<<(std::ostream& os,const Complex& rhs);
};
友元函數(特殊的非成員函數)
- 友元函數是定義在類的外部,而不是類的內部
- 友元函數可以訪問類的所有成員,包括私有和保護成員,就像它們是類的成員一樣
- 友元函數的聲明通常放在類的內部,使用
friend
關鍵字 - 友元函數是通常用于運算符的重載
// 一般非成員函數無法訪問私有成員
void printData(const MyClass& obj) {// 錯誤: 'x' and 'y' are private members of 'MyClass'// std::cout << "x = " << obj.x << ", y = " << obj.y << std::endl;
}
//友元函數
class MyClass {friend void printData(const MyClass& obj);
};
void printData(const MyClass& obj) {// 可以直接訪問 MyClass 的私有成員 x 和 ystd::cout << "x = " << obj.x << ", y = " << obj.y << std::endl;
}
非成員函數
std::istream& operator>>(std::istream& is, Complex& rhs)
std::ostream& operator<<(std::ostream& os,const Complex& rhs)
Complex operator+(const Complex& lhs, const Complex& rhs)
Complex operator+(const Complex& __z, const double __t)
Complex operator+(const double __t, const Complex& __z)
bool operator==(const Complex& __x, const Complex& __y)
bool operator!=(const Complex& __x, const Complex& __y)
類的內存管理
對象
-
內存中連續的存儲空間;
-
重要的特征:具有內存地址;
函數
函數默認為外部鏈接性
函數指針:
// 定義兩個簡單的函數
int add(int a, int b) {return a + b;
}int main() {// 直接定義函數指針int (*fp1)(int, int);// 使用 typedef 定義函數指針類型typedef int (*iifp)(int, int);iifp fp2;// 將函數指針指向 add 函數fp1 = add;fp2 = add;// 調用函數指針指向的函數std::cout << fp1(10, 5) << std::endl; // 輸出 15std::cout << fp2(10, 5) << std::endl; // 輸出 15}
數組
數組名示指向數組首元素的指針;可以通過*(數組名+偏移)訪問數組的任意元素;
字符串數組
-
以’\0’作為結束符的字符數組;
-
長度計算時,結束符不計算在內;
-
既然是數組,可以作為迭代器使用,例如 string s(hello,hello+strlen(hello));
函數原型
int main(int argc, char* argv);
- argc指向命令行參數的數量
- argv存放字符串數組
sizeof的使用:獲取類型的大小
處理文件
1)可以使用重定向方式;2)或者:使用文件方式。
#includ<fstream>ifstream infile(“in”);
ofstream outfile(“out”);
Infile和outfile的使用和cin、cout一樣。
動態分配
使用new/delete進行內存的動態分配/釋放;
C++語言中,分配內存和對象的初始化是緊密聯系的:
int *ip = new int(42);
delete ip;T* p = new T[n];
vector<T> vt<p,p+n);
delete[] vt;
抽象數據類型
正規函數
1)復制構造函數;
2)復制賦值運算符;
3)移動構造函數;
4)移動賦值運算符;
5)析構函數;
6)相等運算符和不等運算符。
class T{
public:T(const T& t);const T& operator=(const T& t);T(T&& t);const T& operator=(T&& t); ~T();//...
};bool operator==(const T& t1, const T& t2);
bool operator!=(const T& t1, const T& t2);
Nice類
定義了上述函數(除不等運算符)的函數稱為Nice類
隱式:implicit
- 復制構造函數;
- 復制賦值運算符;
- 移動構造函數;
- 移動賦值運算符;
- 析構函數。
淺拷貝和深拷貝
-
淺拷貝(Shallow Copy):
- 當我們創建一個類型為
X
的對象時,如果采用默認的拷貝構造函數或賦值運算符,會發生淺拷貝。 - 淺拷貝僅復制
X
對象中的指針成員y1
和y2
,但不會復制它們所指向的Y
對象。 - 因此,拷貝后的對象和原對象共享同一個
Y
對象。 - 這意味著,如果其中一個對象修改了
Y
對象,另一個對象也會受到影響。
- 當我們創建一個類型為
-
深拷貝(Deep Copy):
- 為了避免上述問題,我們需要實現一個深拷貝的拷貝構造函數和賦值運算符。
- 在深拷貝中,不僅要復制
X
對象中的成員變量,還要為每個指針成員創建一個新的Y
對象,并將其賦值給拷貝后的對象。 - 這樣,拷貝后的對象就擁有了自己獨立的
Y
對象,不會受到原對象的影響。
構造vec模板類
template <class T> class Vec{public://接口private:T* data;//Vec中的首元素T* limit;//Vec中的末元素
};
1)類可以控制對象的所有行為
-
創建時:調用構造函數;
-
包含賦值操作的表達式:調用賦值操作符;
-
對象退出或銷毀時:自動調用析構函數。
//復制構造函數
Vec(const Vec&v){create(v.begin(),v.end());}//復制賦值函數
template <class T>
Vec<T>& Vec<T>::operator=(const Vec &rhs)
{if(&rhs != this){uncreate();create(rhs.begin(),rhs.end());}return *this;
}//移動構造函數
Vec(Vec &&v) : _data{v._data}, _limit{v._limit}, _avail{v._avail}
{v._data = v._limit = v._avail = nullptr;
}//移動賦值函數
Vec<T>& Vec<T>::operator=(Vec &&v)
{if(&v != this){uncreate();_data = v._data;_limit = v._limit;_avail = v._avail;v._data = v._limit = v._avail = nullptr;}return *this;
}//析構函數
~Vec(){ uncreate(); }
template <class T>
void Vec<T>::uncreate()
{if (_data!=nullptr){iterator it = _avail;while (it != _data)alloc.destroy(--it);alloc.deallocate(_data, _limit - _data);}_data = _limit = _avail = nullptr;
}//迭代器
typedef T* iterator;
iterator begin(){ return data; }
const_iterator begin()const{ return data; }//索引
T& operator[](size_type i){return data[i];}
const T&operator[](size_type i) const {return data[i];
}
類的數值特征
類型轉換
自動轉換
常見的轉換形式 :通過只帶有一個參數的構造函數實現。
Str s(“hello”); <=> Str t = “hello”;
如果使用了explicit參數,則不能轉換;
- 構造函數的參數是對象的一部分時,不使用explicit;
- 當參數不是對象的一部分時,需要使用explicit。
強制類型轉換
operator int() { return data.size();}
野指針問題
class Str {
public:operator char*();operator const char*() const;
}Str s;
ifstream is(s);
ifstream
接受被強制類型轉換為const char*
類型的s類
- 這個
const char*
指針指向的內存可能是臨時分配的,在Str
對象s
銷毀后就會變成野指針。 - 如果
ifstream
的構造函數試圖使用這個野指針,就會出現未定義行為,很可能導致程序崩潰。
輸入輸出操作
- 上面的形式,>>和<<必須是istream和ostream的成員函數
- 如果把>>和<<作為str的成員函數,那么調用形式必須是s>>cin或s<<cout,這個與庫規則不合!
std::ostream& operator<<(std::ostream& os, const Str& s)
{for (Str::size_type i = 0; i != s.size(); ++i)os << s[i];return os;
}
問題:這個函數需要訪問私有成員數據!
友員函數的訪問權限:和類成員函數一樣!
friend std::istream& operator>>(std::istream&, Str&);
friend std::ostream& operator<<(std::ostream&, const Str&);
兩元運算符
兩元運算符設計成對稱的;
兩元運算符通常設計為友元函數;
Str& operator+=(const Str& s) {copy(s.data.begin(),s.data.end(),back_inserter(data));return *this;
}
Str operator+(const Str& s, const Str& t){Str r = s;r += t;return r;
}
5 面向對象
繼承
抽象基類
class Screen
{
public:virtual void handleInput(sf::RenderWindow& window) = 0;virtual void update(sf::Time delta) = 0;virtual void render(sf::RenderWindow& window) = 0;
};
virtual void func() = 0;定義了一個純虛函數;當一個類中包含純虛函數時,該類就成為抽象基類。抽象基類不能直接實例化對象,而是用來被繼承和實現。
如果派生類沒有實現純虛函數,那么派生類也會成為抽象基類。
繼承的可見性
默認private繼承
- 子類能繼承基類的private對象,但是對于子類不可見,只能通過api訪問
- 派生類定義的方法訪問基類時:派生類的方法可以訪問標記為public、protected、private的方法和屬性;
- public-直接原本地繼承基類
- protected-將所有的public和protected對象聲明為protected
- private-將所有的public和protected對象聲明為private
派生類外部函數僅可訪問public繼承的public函數
class Base {
private:int privateValue;
protected:int protectedValue;
public:Base(int pv, int protv) : privateValue(pv), protectedValue(protv) {}// 公共成員函數,用于訪問 private 成員int getPrivateValue() const {return privateValue;}
};class Derived : public Base {
public:Derived(int pv, int protv) : Base(pv, protv) {}void showValues() {// 無法直接訪問 privateValue// std::cout << "privateValue: " << privateValue << std::endl; // 編譯錯誤// 訪問 protected 成員std::cout << "protectedValue: " << protectedValue << std::endl;// 通過 Base 類的公共成員函數訪問 private 成員std::cout << "privateValue: " << getPrivateValue() << std::endl;}
};
改變可見性
- 對于外部對象:可以使用friend關鍵字,通過設置函數或者類為特定類的friend,運行函數或類訪問特定類的所有成員。
- 對于派生類:可以使用using Base::f的形式,改變其可見性。
- 僅針對protected對象;在基類中的private成員,不能在派生類中用using聲明。
- 可以將基類中的protected對象提升為子類中的public對象
如果希望子類能夠訪問基類的成員,可以使用
protected
訪問修飾符。protected
成員在public繼承時對子類是可見的,而對外部類是不可見的。
//visibility.cpp
class Pet{
protected:char eat() const {return 'a';}int speak() const{return 2;}float sleep() const {return 3.0;}float sleep(int) const {return 4.0;}
};class Goldfish: Pet{
public:using Pet::eat;using Pet::sleep;
};
- 嚴格控制訪問:
Goldfish
類不能訪問Pet
類的private
成員,但可以通過using
聲明將某些protected
成員提升為public
。 - 特化行為:通過選擇性地公開
eat
和sleep
函數,而不公開speak
函數,適應不同派生類的需求。
繼承的類型
單繼承,多繼承(多父類),組合繼承(先單后多),層次繼承(多子類),多級繼承(多單繼承),混合繼承
繼承類對象的創建順序(析構順序相反)
- 先上后下,先左后右
創建步驟:
- 分配內存空間
- 構造屬性對象
- 調用構造函數
類的析構函數是唯一的,僅管理自己的內存
不能繼承的方法
- 構造函數,析構函數,賦值操作符-operator=(…)
向上類型轉換
取一個對象的地址(指針或者應用),并將其作為基類的地址來進行處理,這種處理方法叫做向上類型轉換。
- 將一個類對象的地址綁定到其基類的指針或引用,是合法的
Base* b; //Base class pointerDerived d; //Derived class objectb = &d;b->show(); //調用的是基類的方法
綁定
- 把函數調用和函數體相聯系(重載)
- 靜態綁定(編譯)和動態綁定(運行時)
- 動態綁定的例子(虛函數和重載)
多態
- 一個事物具有多種形式
- 通過重寫抽象父類函數,派生類函數與父類有相同的名字和簽名
- 基類標記為virtual,派生類標記為override
典型的實現
- 編譯器對包含虛函數的類創建一個表,通常叫做vtable;
- 在vtable中放置特定類的虛函數的地址;
- 在每個帶有虛函數的類中,編譯器放置一個指針vptr,讓其指向vtable;
- 當通過基類指針或引用調用虛函數時,編譯器插入代碼,使得能夠通過vptr并在vtable中查到到函數,這樣能調用正確的函數。
逆向工程原理:動態綁定
切片
對象按值傳遞時,會切割出一部分,作為基類對象。
如果在派生類中沒有把析構函數申明為虛析構函數,那么:在使用向上轉換指針或引用時,delete的行為將會只調用基類的析構函數!
因此,在設計具有繼承的類時,通常應當把基類的析構函數定義為虛析構函數!
//class Derived1
~Base1(){ cout<<"~Base1()\n";}
//class Derived2
virtual ~Base2(){ cout<<"~Base2()\n";}Base1* bp = new Derived1;
delete bp;
Base2* b2p = new Derived2;
delete b2p;
bp的刪除只調用了基類的析構函數;
而b2p的刪除則符合我們的希望:先調用Derived2的析構函數,然后再調用Base2的析構函數!
函數重載
基類和派生類
對于符合重載條件的方法
- 如果不是虛函數,則調用相應對象或指針的函數體(靜態綁定)
- 如果是虛函數,在通過指針或引用調用時,調用其實際對象的函數體,即執行動態綁定;
還有一類方法:不符合重載條件的方法,但是同名!
- 函數隱藏:基類中同名的函數,在派生類中都是不可見的!只能使用 base::f()這樣的形式進行調用。
組合
描述類對象之間的component-of關系;
例如:汽車由引擎、車門、輪胎等構成。
類的靜態成員變量
定義方法:在類中定義靜態成員變量時,需加上static關鍵字。
作用:所有的類對象共享該成員變量。通常與類相關的操作。例如,可以使用類的靜態屬性跟蹤某個類在運行的過程中總共創建了多少個實例;或者為每個類的對象建立一個唯一的id。
類的靜態成員變量在使用前必須初始化;且不能在類中直接賦值,需要使用初始化形式(見中間的代碼)。
class MyClass {
public:static int count; // 聲明靜態成員變量MyClass() {count++; // 靜態成員變量的使用}~MyClass() {count--;}
};// 靜態成員變量的初始化
int MyClass::count = 0; // 在類外進行初始化int main() {MyClass obj;std::cout << "Number of objects created: " << MyClass::count << std::endl; // 通過類名訪問靜態成員變量return 0;
}
靜態成員函數
-
調用:使用類名::靜態成員函數()方式或者對象.靜態成員函數()。
-
靜態成員函數的地址是普通的函數指針;而非靜態成員函數的地址則是類成員函數指針(隱含了this指針)
靜態成員函數與非靜態成員函數調用規則
- 在類的非靜態成員函數中可以調用類的靜態成員函數;
- 在類的靜態成員函數中不能調用類的非靜態成員函數;
靜態成員函數不能定義為virtual,const和volatile;
例子:單例模式
class Singleton
{public:static Singleton* getInstance( );~Singleton( );private:Singleton( );inline static Singleton* instance(nullptr);
};
Singleton * Singleton::getInstance() {if (!instance) {instance = new Singleton();};return instance;
};
需要注意的是,這種單例實現在多線程環境下可能會存在線程安全問題,需要額外的同步機制來保證線程安全
補充資料
異常處理
try_catch
try{
...
}
catch(Type1 const& t1){
...
}
catch(Type2 const& t2){
...
}
catch(...){
...
}
catch(…)作為萬能異常處理句柄,可以捕獲任何類型的異常;
noexcept
告知編譯器,函數f和g在執行過程中不會拋出任何未被處理的異常
在函數f和g的內部,它們還是可以拋出異常的,但這些拋出的異常必須在函數內部得到處理。
如果在這些函數中有未被處理的異常,編譯時編譯器將會發出警告。此外,如果在執行這些函數時發生了未被處理的異常,程序將不會回退到函數f或g的調用處,而是會直接調用terminate()函數
void f(int c)
{if (c > std::numeric_limits<char>::max()) {throw std::invalid_argument("f argument too large.");}std::cout << "Never show \n";
}int main() {try{f(256);}catch (std::invalid_argument &e) {std::cout << e.what() << '\n';return -1;}
}
//無noexcept
f argument too large.
//有noexcept
terminate called after throwing an instance of 'std::invalid_argument'
what():f argument too large.
noexcept(false)
與之相對的,noexcept(false)則向編譯器表明,函數h在執行過程中可能會拋出未被處理的異常。
調用函數h之后仍然存在未被處理的異常,程序的執行會回退到函數h的調用點,并繼續進行異常處理,而不是直接調用terminate()函數結束程序的運行。
注意:析構函數默認是不允許拋出未處理的異常的;而構造函數默認是允許拋出未處理的異常的。
~Base() noexcept(false)
#include <iostream>
class Base{
public:
Base() {throw 2023;}
~Base() noexcept(false) {
throw 2024;
}
};
int main(){try {Base *b = new Base();delete b;}catch(int const & s) {std::cout << s << "\n";}catch (Base const& b) {std::cout << &b << std::endl;}std::cout << "after delete\n";return 0;
}
一次try catch只能捕獲最先被扔出的異常
上面程序的輸出為
2023
after delete
期末考試試卷
1.如果有#include <string>,則以下定義錯誤的是:
const std::string exclam(5, “!”);
應為const std::string exclam(5, ‘!’);
const int buf; //未初始化
int cnt = 0;
const int sz = cnt;
++cnt; ++sz; //不能對sz進行修改
二、程序閱讀題
void f(int& x, int *y, int z) {z = x + *y / 2;*y = x + z / 2;x = *y + z / 2;
}
int main() {int a = 5, b = 7, c = 11;f(a, &b, c); //f(5,7,11)cout << a << ',' << b << ',' << c;return 0;
}//函數中z=8,*y=9,x=13,輸出為13,9,11
class Base {char c1, c2;
public:Base(char n = 'a') :c1(n), c2(n + 2) {}virtual ~Base() {cout << c1 << c2 << '\n';}
};
class Derived :public Base {char c3;
public:Derived(char n = 'A') :Base(n + 1), c3(n) {}~Derived() { cout << c3; }
};
int main() {Derived* a = new Derived[2];delete[] a;
}// Derived(A):Base(B):c1(B),v2(D),c3(A)
//ABD(換行)
//ABD(換行)
class Abc
{int value_;static const int SPAN = 0x64;
public:static int seq;Abc() :value_(++seq) {}Abc(const Abc& rhs) :value_(rhs.value_ + SPAN) { ++seq; }Abc& operator=(const Abc& rhs) {seq++;value_ = rhs.value_ / 2;return *this;}int value() const { return value_; }
};
int Abc::seq;
int main()
{Abc m, n, p, q(p);m = q;
}//答案:seq=5 m.value=100+3/2=51
class Value {int value_;
public:Value(int v = 0) : value_(v % 7) {}int value() const { return value_; }
};
bool filter(Value const& v) {cout << v.value() << ' ';return v.value() % 5 == 0;
}
void output(Value const& v) {cout << v.value() << ' ';
}
int main() {int a[] = { 20, 25, 30, 35, 40, 45, 50 };vector<Value> values(a, a + sizeof a / sizeof a[0]);vector<Value> filtered(values.size() / 2);copy_if(values.begin(), values.end(), back_inserter(filtered),filter);cout << '\n';for (vector<Value>::iterator itr = filtered.begin(); itr !=filtered.end(); itr++)output(*itr);
}
//20 25 30 35 40 45 50 %7
//back_inserter作用是在最后插入
//6 4 2 0 5 3 1
//0 0 0 0 5 //前3個是初始化自帶的
三、程序填空題
template<class T>//1.
class LoopQueue {
public:typedef typename vector<T>::size_type size_type;LoopQueue(int capacity):data(capacity+1){first = last = 0;}bool isEmpty()const { return first == last; }bool isFull()const { return (last + 1) % data.size() == first; }size_type getLength()const {if (last >= first//3.) return last - first;return last - first + data.size();}bool dequeue(T& e) {if (isEmpty()) return false;e=vector[first] //4;first=(first+1)%data.size()//5;return true;}bool enqueue(const T& e) {if (isFull()) return false;vector[last]=e //6;last=(last+1)%data.size()//7;return true;}void print() {size_type i;for (i = first; i != last; i = (i + 1) % data.size()) {cout<<"<"<<data[i]<<">";//8;}cout << endl;}
private:
vector<T> data;
size_type first, last;
};
int main()
{int a;LoopQueue<int> qu(3);for (int i = 1; i < 6; i++) {qu.enqueue(i);}qu.dequeue(a);cout << qu.getLength() << endl;qu.enqueue(a);cout << qu.getLength() << endl;qu.print();return 0;
}
四、編程題
template <typename ForwardIterator>
void Rotate(ForwardIterator begin, ForwardIterator mid, ForwardIterator end) {if (begin == mid || mid == end) return;// Calculate the distances//auto f=std::distance(begin,mid);//auto t=std::distance(begin,end);std::vector<typename std::iterator_traits<ForwardIterator>::value_type> temp(begin, mid);//typename的作用是告訴編譯器后面是類型名ForwardIterator dest = begin;ForwardIterator src = mid;while (src != end) {*dest++ = std::move(*src++);}src = temp.begin();while (dest != end) {*dest++ = std::move(*src++);}
}
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
struct candidate{int id;int score;
};
class candidates{
public:candidates(){}void readinfo(istream& in){candidate c;while(in>>c.id>>c.score){candidates.push_back(c);}if (in.eof()) {// Handle end of file gracefullycout << "End of file reached." << endl;std::cin.clear(); // Clear fail state} else if (in.fail()) {// Handle input failurecerr << "Input failure occurred." << endl;}}void sort_by_score_ID(){sort(candidates.begin(),candidates.end(),[](const candidate& c1,const candidate& c2){if(c1.score>c2.score)return true;//降序排列return c1.ID<c2.ID;});}void calculate(int n){if(n>candidates.size()){cerr<<"error"<<n<<endl;exit(1);}scoreLine=candidates[n-1].score;std::copy_if(candidates.begin(),candidates.end(),std::back_inserter(access),[this](const candidate& c){return c.score>=scoreLine; });}void output(ostream& out){for(auto&a:access){out<<a.id<<a.score<<std::endl;}}int get_mps(){return scoreLine;}int get_num(){return access.size();}
private:std::vector<candidate> candidates;std::vector<candidate> access;int scoreLine;
};
int main()
{candidates cs;cout << "Enter candidates' info: " << endl;cs.readinfo(cin);cs.sort_by_score_ID();cout << "Enter the number of summer camps to be recruited: ";int n;cin >> n;cs.calculate(n);// cout << "The minimum passing score: " << cs.get_mps() << endl;// cout << "Number of interviewees: " << cs.get_num() << endl;cout << "Info of the interviewees: " << endl;cs.output(cout);
}