banner
Nanomoa

Nanomoa's Blog

"Bytes and Brackets: My Journey in Go and C++ Exploration."
github
zhihu
telegram
twitter

CS106L 春季 2024 學習筆記

課程首頁: CS106L

1. 歡迎#

這節課主要是對 CS106L 課程的簡單介紹,作業安排,課程內容的羅列,C++ 發展歷史的介紹.

課程介紹:#

這門課主要介紹 C++ 的現代特性,對基本語法基本沒有耗費時間,需要一定的語言基礎.

作業安排:#

這門課會有七個簡單的小作業,目前開放的作業如下:

作業序號作業標題作業連結
Assignment 1SimpleEnroll前往完成
Assignment 2Marriage Pact前往完成
Assignment 3Make a class!前往完成
Assignment 4Weather Forecast前往完成
.........

課程內容:#

編號課程內容
1C++ feature 簡介
2初始化,引用,Streams
3容器,迭代器,指針
4類,模板類,const
5模板函數,函數,lambda
6運算符,特殊成員函數
7move 語義,類型安全

發展歷史:#

參照第一節課的 slides 即可.

2. 類型和結構體#

類型:#

基本類型:#

C++ 提供了一下幾種基本類型:

int val = 5; //32 bits
char ch = 'F'; //8 bits
float decimalVal1 = 5.0; //32 bits
double decimalVal2 = 5.0; //64 bits
bool bVal = true; //1 bit

string 頭文件中實現了對 string 類型的支持

std::string str = "Nanomoa";

C++ 是一門靜態類型語言,這意味著任何一個變數 / 函數等等都應該在運行前顯示的聲明其類型.

動態類型和靜態類型的區別:

  • 動態類型: 在執行代碼是一行一行的檢查類型,可能在運行時出現一些錯誤
  • 靜態類型: 在運行之前,編譯時檢查類型,編譯錯誤將無法運行,且靜態類型語言的代碼可讀性更高

在 C++ 中不能定義多個相同的函數,但是可以通過函數重載實現其不同版本.

函數重載需要滿足函數名相同,但 返回值類型 / 參數列表中參數的類型 / 參數列表中參數的數量 三者至少有一項不相同.

結構體:#

auto 關鍵字的引入:#

用於代替實際類型,讓編譯器自動根據變數的值推斷其類型.

auto a = 1; // int
auto b = 3.14; // double
auto c = 'a'; // char
auto d = "Hello"; // char*

結構體:#

結構體是一組變數的集合,其中每個字段都有它們自己的類型.

示例:

struct Student {
    std::string name;
    int age;
};

Student stu;
stu.name = "Nanomoa";
stu.age = 20;

結構體可以作為參數傳遞一組信息,或者作為返回值 (可以通過結構體使得函數間接的具有多個返回值).

示例:

void printStudentInfo(Student s) {
    std::cout << s.name << " " << s.age << std::endl;
}

Student getStudent() {
    Student s;
    s.name = "Test";
    s.age = 20;
    return s;
}

Student s = getStudent();
printStudentInfo(s);

結構體初始化:

Student s;
s.name = "A";
s.age = 20;

// or
s = {"A", 20};

std::pair 引入:

std::pair 是 STL 中提供的一個模板,可以接受任意一對不同類型,其包含兩個字段 firstsecond.

示例:

std::pair<int, std::string> val = {1, "AA"};
std::cout << val.first << " " << val.second << std::endl; // "1 AA"

可以配合 std::pair 實現如下返回值攜帶操作狀態的函數:

std::pair<bool, std::string> func() {
    bool is_success = false;
    // todo ...
    if(is_success) {
        return {true, "Success Info"};
    } else {
        return {false, "Failed Info"};
    }
}

std::pair<bool, std::string> res = func();
auto res1 = func();

3. 初始化 & 引用#

初始化:#

直接初始化:#

方式如下:

int valInt = 1;
int valInt1(2);

注意: C++ 不在乎初始化傳入的數值是否為整數 (傳入浮點數會轉換成整數)

例:

int valInt = 3.00; // valInt => 3
int valInt1(4.5); // valInt1 => 4

Uniform initialization:#

C++11 開始支持,更安全,會進行類型檢查,例:

int valInt{1}; // 正確
int valInt1{3.14}; // 無法通過編譯

使用:

// std::map
std::map<std::string, int> ages{
    {"Nanomoa", 20},
    {"Test", 21}
};

// std::vector
std::vector<int> vec{1, 2, 3, 4, 5};

// struct
struct Student {
    std::string name;
    int age;
};

Student stu;
stu.name = "Nanomoa";
stu.age = 20;
// 等同於下面
Student s = {"Nanomoa", 20};

結構化綁定:#

通過對象的元素或成員初始化多個實體,C++17 開始支持,例:

std::tuple<std::string, std::string, std::string> getClassInfo() {
    std::string className = "CS106L";
    std::string buildingName = "Turing Auditorium";
    std::string language = "C++";
    return {className, buildingName, language};
}

auto classInfo = getClassInfo();
std::string className = std::get<0>(classInfo);
std::string buildingName = std::get<1>(classInfo);
std::string language = std::get<2>(classInfo);
// 等同於下面(結構化綁定)
auto [className, buildingName, language] = getClassInfo();

引用:#

引用是已存在事物的別名,符號 &, 例:

int num = 5;
int& ref = num;
ref = 10;
std::cout << ref << ' ' << num << std::endl; // 10 10

引用作為函數參數時也有同樣效果:

void func(int& n) {
    n = std::pow(n, 2);
}

int main() {
    int num = 2;
    func(2);
    std::cout << num << std::endl; // 4
    return 0;
}
// 傳入此函數中的nums並沒有被修改
void func(std::vector<std::pair<int, int>> &nums) {
    for (auto [num1, num2] : nums) { // 此處通過結構化綁定copy了nums中當前pair的值
        num1++; // 此處修改的是copy的pair, 而不是nums中的pair
        num2++;
    }
}

// 此函數中 nums 被修改
void func1(std::vector<std::pair<int, int>> &nums) {
    for (auto& [num1, num2] : nums) { // 此處通過結構化綁定引用了nums中當前pair
        num1++;
        num2++;
    }
}

左值 / 右值:#

  • 左值:可以在等號左邊,也可以在等號右邊
  • 右值:只能在等號右邊

例:

// x 可以作為左值
int y = x;
x = 10;
// 10 可以作為右值
int y = 10;
10 = x; // 錯誤

注意: 不能將右值綁定到非 const 的左值,例:

int squareN(int& num) {
    return std::pow(num, 2);
}

auto val = squareN(2); // 編譯錯誤

Const:#

使用 const 聲明的對象不可修改,例:

std::vector<int> vec{1, 2, 3}; // 非 const
vec.push_back(4); // 可修改
std::vector<int>& ref_vec{vec}; // 引用
ref_vec.push_back(5);
const std::vector<int> const_vec{1, 2, 3}; // const
const_vec.push_back(4); // 編譯錯誤, 不可修改
const std::vector<int>& ref_const_vec{const_vec}; // const 引用
ref_const_vec.push_back(4); // 不可修改

注意: 對於 const 聲明的對象,不能為其聲明 非const 的引用,例:

const std::vector<int> vec{1, 2, 3};
std::vector<int>& ref_vec{vec}; // 錯誤
const std::vector<int>& const_ref_vec{vec}; // 正確

如何編譯 C++:#

g++ -std=c++11 main.cpp -o main

or

g++ -std=c++11 main.cpp

4. Streams#

Streams 是什麼:#

C++ 通用的輸入輸出抽象

cin 和 cout:#

位於 iostream 頭文件中

cerr 和 clog#
  • cerr: 用於輸出 errors
  • clog: 用於記錄非關鍵事件日誌
cout 和 cin#

std::cout 和 IO 庫:

std::cout << "Hello, World" << std::endl;

std::cout 是一個 stream

std::cout stream 是 std::ostream 的一個實例,是標準輸出流

一個輸入流:

"3.14" -> 輸入流 -> 類型轉換 -> (double)(3.14)

double pi;
std::cin >> pi;
std::cout << pi / 2 << std::endl;

std::cin 是控制台 輸入流

std::cout stream 是 std::istream 的一個實例,是標準輸入流

stringstreams:#

std::stringstream#

一種將字符串看作流的方法,可用於處理混合類型的數據

std::string initial_quote = "Bjarne Stroustrup C makes it easy to shoot yourself in the foot";
std::stringstream ss(initial_quote); // 也可以 ss << initial_quote;
std::string first,
            last,
            language,
            extracted_quote;
ss >> first >> last >> language >> extracted_quote;
std::cout << first << " " << last << " " << language << " " << extracted_quote << std::endl;
first => Bjarne 
last => Stroustrup 
language => C 
extracted_quote => makes

extracted_quote => makes , 注意,使用 >> 運算符只會讀取到下一個空格前

如果我們需要讀取包括 makes 後面的所有內容,應該使用 getline(), 例:

std::string initial_quote = "Bjarne Stroustrup C makes it easy to shoot yourself in the foot";

std::stringstream ss(initial_quote);

std::string first,
            last,
            language,
            extracted_quote;
ss >> first >> last >> language;
std::getline(ss, extracted_quote);
std::cout << first << " " << last << " " << language << " " << extracted_quote << std::endl;
first => Bjarne 
last => Stroustrup 
language => C 
extracted_quote => makes it easy to shoot yourself in the foot

輸出流:#

一種寫入數據的方式

double tao = 6.28;
std::cout << tao;

tao -> std::cout stream -> "6.28" -> flush -> console/others

std::cout stream 是 line-buffered, buffer 中的內容在flush前不會顯示在目標上

line-buffered: buffer 在遇到 '\n' 時會被刷新

例:

std::cout << "Starting intense computation...";
std::this_thread::sleep_for(std::chrono::seconds(5));
std::cout << "finished computation\n";

這段代碼的效果並不是 先輸出 "Starting intense computation..." 等待 5s 後再輸出 "finished computation"

而是等待 5s 後同時輸出 "Starting intense computation..." 和 "finished computation"

因為 std::cout << "Starting intense computation..."; 沒有 flush

解決該問題的方法:

std::cout << "Starting intense computation...\n";
// or
std::cout << "Starting intense computation..." << std::endl;
// or
std::cout << "Starting intense computation...";
std::cout << std::flush;

std::ios::sync_with_stdio(false) 用於取消 C++ 的標準流(例如 cout 和 cin)和 C 的標準 IO(例如 printf 和 scanf)之間的同步,可以提升 IO 的性能

但注意:禁用同步後不可將 C++ 流與 C 的標準 IO 混用,否則會導致 ub

輸出文件流:#

std::ofstream, 一種向文件寫入數據的方式,常用方法有is_open(), open(), close(), fail()

例:

std::ofstream ofs("hello.txt"); // 為 hello.txt 創建一個流, 如果文件不存在會先新建該文件
if (ofs.is_open()) { // 判斷是否打開
    ofs << "Hello CS106L" << '\n'; // 如果打開則向文件中寫入內容
}
ofs.close(); // 關閉, 關閉後不可操作
ofs.open("hello.txt"); // 打開
ofs << "Open Again" << '\n'; // 寫入內容

我們期望的文件內容:

Hello CS106L
Open Again

實際為:

Open Again

如果需要追加內容而不是覆蓋內容,應該按照如下方式打開:

ofs.open("hello.txt", std::ios::app);

輸入文件流:#

std::ifstream, 一種從文件中讀取內容的方式,用法與 std::ofstream 基本一致

例:

std::ifstream ifs("hello.txt");
if (ifs.is_open()) {
    std::string line1;
    std::getline(ifs, line1);
    std::cout << line1 << '\n';
}
if (ifs.is_open()) {
    std::string line2;
    std::getline(ifs, line2);
    std::cout << line2 << '\n';
}

輸入流:#

std::istream, 一種讀取數據的方式

std::cinbuffered, 每次讀取到下一个 ' ' or '\n' or '\t' 位置

例:

/*
3,14 6.28
Nanomoa AAAA
*/
double pi, tao;
std::string name;
std::cin >> pi >> tao;
std::cin >> name;
std::cout << pi << " " << tao << " " << name << '\n';
// 3.14 6.28 Nanomoa

我們如果要使得name為 "Nanomoa AAAA", 應該使用 std::getline():

double pi, tao;
std::string name;
std::cin >> pi >> tao;
std::getline(std::cin, name);
std::cout << pi << " " << tao << " " << name << '\n';

但是我們發現,讀取完 pi 和 tao 程序就結束了

因為此處的 std::getline(std::cin, name) 讀取的是tao 後面的 '\n'

所以,我們應該先讀取空格後再讀取 name:

double pi, tao;
std::string name;
std::cin >> pi >> tao;
std::getline(std::cin, name);
std::getline(std::cin, name);
std::cout << pi << " " << tao << " " << name << '\n';

但是,不推薦 std::getlinestd::cin 同時使用,因為二者解析數據的方式不同

非要共用的情況下可以使用 std::getline 配合 std::stringstream 代替:

double pi, tao;
std::string name;
std::string line;
std::getline(std::cin, line);
std::stringstream ss(line);
ss >> pi >> tao;
std::getline(std::cin, line);
name = line;
std::cout << pi << " " << tao << " " << name << std::endl;

5. 容器#

(更新中...)

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。