├── README.md ├── C++多文件编程.pdf ├── 面向对象程序设计基础.pdf ├── Modern C++ 选讲.pdf ├── 编译、链接、静态库、动态库.pdf ├── Makefile & CMake.pdf ├── Compile ├── code │ ├── invsqrt.h │ ├── main.cpp │ └── invsqrt.cpp └── README.md ├── Compile_ToolChains ├── 01-basic │ ├── main.cpp │ └── CMakeLists.txt ├── 05-third-party │ ├── include │ │ └── mysqrt.h │ ├── src │ │ ├── mysqrt.cpp │ │ └── main.cpp │ └── CMakeLists.txt ├── 00-makefile │ ├── invsqrt.h │ ├── main.cpp │ ├── makefile │ └── invsqrt.cpp ├── 02-multifiles │ ├── include │ │ └── invsqrt.h │ ├── src │ │ ├── main.cpp │ │ └── invsqrt.cpp │ └── CMakeLists.txt ├── 03-static-libraries │ ├── include │ │ └── invsqrt.h │ ├── src │ │ ├── main.cpp │ │ └── invsqrt.cpp │ └── CMakeLists.txt ├── 04-shared-libraries │ ├── include │ │ └── invsqrt.h │ ├── src │ │ ├── main.cpp │ │ └── invsqrt.cpp │ └── CMakeLists.txt ├── exercises.md └── README.md ├── Object_Oriented_Programming ├── exercises.md ├── LSP.cpp ├── DIP_before.cpp ├── ISP_after.cpp ├── DIP_after.cpp ├── ISP_before.cpp ├── loose_coupling.cpp ├── Bridge_before.cpp ├── Bridge_after.cpp └── README.md ├── .gitignore ├── .vscode └── settings.json ├── .clang-format ├── Multi-file_Programming └── README.md └── Modern_Cpp └── README.md /README.md: -------------------------------------------------------------------------------- 1 | # Cpp-tutorial 2 | 清华大学电子工程系软件部暑培——C++部分 3 | -------------------------------------------------------------------------------- /C++多文件编程.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ther-nullptr/Cpp-tutorial/HEAD/C++多文件编程.pdf -------------------------------------------------------------------------------- /面向对象程序设计基础.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ther-nullptr/Cpp-tutorial/HEAD/面向对象程序设计基础.pdf -------------------------------------------------------------------------------- /Modern C++ 选讲.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ther-nullptr/Cpp-tutorial/HEAD/Modern C++ 选讲.pdf -------------------------------------------------------------------------------- /编译、链接、静态库、动态库.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ther-nullptr/Cpp-tutorial/HEAD/编译、链接、静态库、动态库.pdf -------------------------------------------------------------------------------- /Makefile & CMake.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ther-nullptr/Cpp-tutorial/HEAD/Makefile & CMake.pdf -------------------------------------------------------------------------------- /Compile/code/invsqrt.h: -------------------------------------------------------------------------------- 1 | #ifndef INV_SQRT_H 2 | #define INV_SQRT_H 3 | 4 | float Q_rsqrt( float number ); 5 | 6 | #endif // INV_SQRT_H -------------------------------------------------------------------------------- /Compile_ToolChains/01-basic/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int main() 4 | { 5 | std::cout << "hello world!" << std::endl; 6 | } -------------------------------------------------------------------------------- /Compile_ToolChains/05-third-party/include/mysqrt.h: -------------------------------------------------------------------------------- 1 | #ifndef MYSQRT_H 2 | #define MYSQRT_H 3 | 4 | double mysqrt(double x); 5 | 6 | #endif // SQRT_H -------------------------------------------------------------------------------- /Compile_ToolChains/00-makefile/invsqrt.h: -------------------------------------------------------------------------------- 1 | #ifndef INV_SQRT_H 2 | #define INV_SQRT_H 3 | 4 | float Q_rsqrt( float number ); 5 | 6 | #endif // INV_SQRT_H -------------------------------------------------------------------------------- /Compile_ToolChains/02-multifiles/include/invsqrt.h: -------------------------------------------------------------------------------- 1 | #ifndef INV_SQRT_H 2 | #define INV_SQRT_H 3 | 4 | float Q_rsqrt( float number ); 5 | 6 | #endif // INV_SQRT_H -------------------------------------------------------------------------------- /Compile_ToolChains/03-static-libraries/include/invsqrt.h: -------------------------------------------------------------------------------- 1 | #ifndef INV_SQRT_H 2 | #define INV_SQRT_H 3 | 4 | float Q_rsqrt( float number ); 5 | 6 | #endif // INV_SQRT_H -------------------------------------------------------------------------------- /Compile_ToolChains/04-shared-libraries/include/invsqrt.h: -------------------------------------------------------------------------------- 1 | #ifndef INV_SQRT_H 2 | #define INV_SQRT_H 3 | 4 | float Q_rsqrt( float number ); 5 | 6 | #endif // INV_SQRT_H -------------------------------------------------------------------------------- /Compile_ToolChains/exercises.md: -------------------------------------------------------------------------------- 1 | # 作业(无需提交) 2 | 3 | 以下是THUAI5工程中使用到的CMakeLists.txt文件: 4 | 5 | [CMakeLists.txt](https://github.com/eesast/THUAI5/blob/dev/CAPI/CMakeLists.txt) 6 | 7 | 感兴趣的同学可以阅读这些文件,了解课上所讲的CMake语法在实际项目中的应用。 -------------------------------------------------------------------------------- /Object_Oriented_Programming/exercises.md: -------------------------------------------------------------------------------- 1 | # 作业(无需提交) 2 | 3 | 以下是THUAI5工程中使用到OOP设计思想的一个例子,感兴趣的同学可以阅读了解,了解课上所讲的思想在实际项目中的应用(当然,现在THUAI5的架构还有很多提升的空间!): 4 | 5 | [关于THUAI5 CAPI部分架构的一些思考 #102](https://github.com/eesast/THUAI5/issues/102) -------------------------------------------------------------------------------- /Compile/code/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "invsqrt.h" 3 | 4 | int main() 5 | { 6 | float x; 7 | 8 | while(1) 9 | { 10 | std::cout << "input a number(press Ctrl+C to exit):"; 11 | std::cin >> x; 12 | std::cout << Q_rsqrt(x) << std::endl; 13 | } 14 | } -------------------------------------------------------------------------------- /Compile_ToolChains/00-makefile/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "invsqrt.h" 3 | 4 | int main() 5 | { 6 | float x; 7 | while(1) 8 | { 9 | std::cout << "input a number(press Ctrl+C to exit):"; 10 | std::cin >> x; 11 | std::cout << Q_rsqrt(x) << std::endl; 12 | } 13 | } -------------------------------------------------------------------------------- /Compile_ToolChains/05-third-party/src/mysqrt.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "../include/mysqrt.h" 3 | 4 | double mysqrt(double x) 5 | { 6 | double b = sqrt(x); 7 | if (b != b) // NaN check 8 | { 9 | return -1.0; 10 | } 11 | else 12 | { 13 | return sqrt(x); 14 | } 15 | } -------------------------------------------------------------------------------- /Compile_ToolChains/01-basic/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Set the minimum version of CMake that can be used 2 | # To find the cmake version run 3 | # $ cmake --version 4 | cmake_minimum_required(VERSION 3.5) 5 | 6 | # Set the project name 7 | project (hello_cmake) 8 | 9 | # Add an executable 10 | add_executable(hello main.cpp) 11 | -------------------------------------------------------------------------------- /Compile_ToolChains/02-multifiles/src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "../include/invsqrt.h" 3 | 4 | int main() 5 | { 6 | float x; 7 | while(1) 8 | { 9 | std::cout << "input a number(press Ctrl+C to exit):"; 10 | std::cin >> x; 11 | std::cout << Q_rsqrt(x) << std::endl; 12 | } 13 | } -------------------------------------------------------------------------------- /Compile_ToolChains/03-static-libraries/src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "../include/invsqrt.h" 3 | 4 | int main() 5 | { 6 | float x; 7 | while(1) 8 | { 9 | std::cout << "input a number(press Ctrl+C to exit):"; 10 | std::cin >> x; 11 | std::cout << Q_rsqrt(x) << std::endl; 12 | } 13 | } -------------------------------------------------------------------------------- /Compile_ToolChains/04-shared-libraries/src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "../include/invsqrt.h" 3 | 4 | int main() 5 | { 6 | float x; 7 | while(1) 8 | { 9 | std::cout << "input a number(press Ctrl+C to exit):"; 10 | std::cin >> x; 11 | std::cout << Q_rsqrt(x) << std::endl; 12 | } 13 | } -------------------------------------------------------------------------------- /Compile_ToolChains/00-makefile/makefile: -------------------------------------------------------------------------------- 1 | CXXFLAGS = -std=c++17 -O2 2 | 3 | main: main.o invsqrt.o 4 | $(CXX) $(CXXFLAGS) -o $@ $^ 5 | 6 | main.o: main.cpp invsqrt.h 7 | $(CXX) $(CXXFLAGS) -o $@ -c $< 8 | 9 | invsqrt.o: invsqrt.cpp invsqrt.h 10 | $(CXX) $(CXXFLAGS) -o $@ -c $< 11 | 12 | .PHONY: clean 13 | clean: 14 | rm main.o invsqrt.o main -------------------------------------------------------------------------------- /Compile_ToolChains/03-static-libraries/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.5) 2 | project(invsqrt) 3 | 4 | # create static library 5 | add_library(invsqrt_static STATIC src/invsqrt.cpp) 6 | target_include_directories(invsqrt_static PUBLIC ${PROJECT_SOURCE_DIR}/include) 7 | 8 | # create executable 9 | add_executable(invsqrt src/main.cpp) 10 | target_link_libraries(invsqrt PRIVATE invsqrt_static) 11 | -------------------------------------------------------------------------------- /Compile_ToolChains/04-shared-libraries/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.5) 2 | project(invsqrt) 3 | 4 | # create shared library 5 | add_library(invsqrt_shared SHARED src/invsqrt.cpp) 6 | target_include_directories(invsqrt_shared PUBLIC ${PROJECT_SOURCE_DIR}/include) 7 | 8 | # create executable 9 | add_executable(invsqrt src/main.cpp) 10 | target_link_libraries(invsqrt PRIVATE invsqrt_shared) 11 | -------------------------------------------------------------------------------- /Compile_ToolChains/00-makefile/invsqrt.cpp: -------------------------------------------------------------------------------- 1 | float Q_rsqrt(float number) 2 | { 3 | long i; 4 | float x2, y; 5 | const float threehalfs = 1.5F; 6 | 7 | x2 = number * 0.5F; 8 | y = number; 9 | i = *(long *)&y; // evil floating point bit level hacking 10 | i = 0x5f3759df - (i >> 1); // what the fuck? 11 | y = *(float *)&i; 12 | y = y * (threehalfs - (x2 * y * y)); // 1st iteration 13 | return y; 14 | } -------------------------------------------------------------------------------- /Compile_ToolChains/02-multifiles/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # build part 2 | cmake_minimum_required(VERSION 3.5) 3 | project (invsqrt) 4 | set(SOURCES src/invsqrt.cpp src/main.cpp) 5 | add_executable(invsqrt ${SOURCES}) 6 | target_include_directories(invsqrt PUBLIC ${PROJECT_SOURCE_DIR}/include) 7 | 8 | # debug part 9 | message("CMAKE_SOURCE_DIR: ${CMAKE_SOURCE_DIR}") 10 | message("PROJECT_SOURCE_DIR: ${PROJECT_SOURCE_DIR}") 11 | 12 | message("SOURCES: ${SOURCES}") -------------------------------------------------------------------------------- /Compile_ToolChains/02-multifiles/src/invsqrt.cpp: -------------------------------------------------------------------------------- 1 | #include "../include/invsqrt.h" 2 | 3 | float Q_rsqrt(float number) 4 | { 5 | long i; 6 | float x2, y; 7 | const float threehalfs = 1.5F; 8 | 9 | x2 = number * 0.5F; 10 | y = number; 11 | i = *(long *)&y; // evil floating point bit level hacking 12 | i = 0x5f3759df - (i >> 1); // what the fuck? 13 | y = *(float *)&i; 14 | y = y * (threehalfs - (x2 * y * y)); // 1st iteration 15 | return y; 16 | } -------------------------------------------------------------------------------- /Compile_ToolChains/03-static-libraries/src/invsqrt.cpp: -------------------------------------------------------------------------------- 1 | #include "../include/invsqrt.h" 2 | 3 | float Q_rsqrt(float number) 4 | { 5 | long i; 6 | float x2, y; 7 | const float threehalfs = 1.5F; 8 | 9 | x2 = number * 0.5F; 10 | y = number; 11 | i = *(long *)&y; // evil floating point bit level hacking 12 | i = 0x5f3759df - (i >> 1); // what the fuck? 13 | y = *(float *)&i; 14 | y = y * (threehalfs - (x2 * y * y)); // 1st iteration 15 | return y; 16 | } -------------------------------------------------------------------------------- /Compile_ToolChains/04-shared-libraries/src/invsqrt.cpp: -------------------------------------------------------------------------------- 1 | #include "../include/invsqrt.h" 2 | 3 | float Q_rsqrt(float number) 4 | { 5 | long i; 6 | float x2, y; 7 | const float threehalfs = 1.5F; 8 | 9 | x2 = number * 0.5F; 10 | y = number; 11 | i = *(long *)&y; // evil floating point bit level hacking 12 | i = 0x5f3759df - (i >> 1); // what the fuck? 13 | y = *(float *)&i; 14 | y = y * (threehalfs - (x2 * y * y)); // 1st iteration 15 | return y; 16 | } -------------------------------------------------------------------------------- /Compile_ToolChains/05-third-party/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 2.6) 2 | 3 | project(cmake_with_gtest) 4 | 5 | set(SOURCES src/mysqrt.cpp src/main.cpp) 6 | 7 | find_package(GTest) 8 | message("GTEST_LIBRARIES: ${GTEST_LIBRARIES}") 9 | message("GTEST_INCLUDE_DIRS: ${GTEST_INCLUDE_DIRS}") 10 | 11 | include_directories(${GTEST_INCLUDE_DIRS} ${PROJECT_SOURCE_DIR}/include) 12 | 13 | add_executable(cmake_with_gtest ${SOURCES}) 14 | target_link_libraries(cmake_with_gtest ${GTEST_LIBRARIES} pthread) -------------------------------------------------------------------------------- /Compile_ToolChains/05-third-party/src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "../include/mysqrt.h" 3 | 4 | TEST(mysqrtTest, PositiveNos) 5 | { 6 | ASSERT_EQ(6, mysqrt(36.0)); 7 | ASSERT_EQ(18.0, mysqrt(324.0)); 8 | ASSERT_EQ(25.4, mysqrt(645.16)); 9 | ASSERT_EQ(0, mysqrt(0.0)); 10 | } 11 | 12 | TEST(mysqrtTest, NegativeNos) 13 | { 14 | ASSERT_EQ(-1.0, mysqrt(-15.0)); 15 | ASSERT_EQ(-1.0, mysqrt(-0.2)); 16 | } 17 | 18 | int main(int argc, char **argv) 19 | { 20 | testing::InitGoogleTest(&argc, argv); 21 | return RUN_ALL_TESTS(); 22 | } -------------------------------------------------------------------------------- /Object_Oriented_Programming/LSP.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | class Rectangle 4 | { 5 | public: 6 | Rectangle(int width, int height) : width(width), height(height) {} 7 | 8 | void setWidth(int width) 9 | { 10 | this->width = width; 11 | } 12 | 13 | void setHeight(int height) 14 | { 15 | this->height = height; 16 | } 17 | 18 | void setEdges(int width, int height) 19 | { 20 | this->width = width; 21 | this->height = height; 22 | } 23 | 24 | private: 25 | int width; 26 | int height; 27 | }; 28 | 29 | class Square: public Rectangle 30 | { 31 | // ... 32 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Compiled Object files 5 | *.slo 6 | *.lo 7 | *.o 8 | *.obj 9 | 10 | # Precompiled Headers 11 | *.gch 12 | *.pch 13 | 14 | # Compiled Dynamic libraries 15 | *.so 16 | *.dylib 17 | *.dll 18 | 19 | # Fortran module files 20 | *.mod 21 | *.smod 22 | 23 | # Compiled Static libraries 24 | *.lai 25 | *.la 26 | *.a 27 | *.lib 28 | 29 | # Executables 30 | *.exe 31 | *.out 32 | *.app 33 | 34 | CMakeLists.txt.user 35 | CMakeCache.txt 36 | CMakeFiles 37 | CMakeScripts 38 | Testing 39 | Makefile 40 | cmake_install.cmake 41 | install_manifest.txt 42 | compile_commands.json 43 | CTestTestfile.cmake 44 | _deps 45 | 46 | build/ 47 | 48 | *.i 49 | *.s -------------------------------------------------------------------------------- /Compile/code/invsqrt.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #define DEBUG 3 | #define PI 3.14 4 | 5 | float Q_rsqrt(float number) 6 | { 7 | long i; 8 | float x2, y; 9 | const float threehalfs = 1.5F; 10 | 11 | #ifdef DEBUG 12 | std::cout << "Debug version" << std::endl; 13 | #else 14 | std::cout << "Release version" << std::endl; 15 | #endif 16 | std::cout << PI << std::endl; 17 | 18 | x2 = number * 0.5F; 19 | y = number; 20 | i = *(long *)&y; // evil floating point bit level hacking 21 | i = 0x5f3759df - (i >> 1); // what the fuck? 22 | y = *(float *)&i; 23 | y = y * (threehalfs - (x2 * y * y)); // 1st iteration 24 | return y; 25 | } -------------------------------------------------------------------------------- /Object_Oriented_Programming/DIP_before.cpp: -------------------------------------------------------------------------------- 1 | 2 | class Account; 3 | 4 | class Customer 5 | { 6 | public: 7 | // ... 8 | void setAccount(Account *account) 9 | { 10 | customerAccount = account; 11 | } 12 | // ... 13 | private: 14 | Account *customerAccount; 15 | }; 16 | 17 | class Account 18 | { 19 | public: 20 | void setOwner(Customer *customer) 21 | { 22 | owner = customer; 23 | } 24 | 25 | private: 26 | Customer *owner; 27 | }; 28 | 29 | int main() 30 | { 31 | Account* account = new Account { }; 32 | Customer* customer = new Customer { }; 33 | account->setOwner(customer); 34 | customer->setAccount(account); 35 | } -------------------------------------------------------------------------------- /Object_Oriented_Programming/ISP_after.cpp: -------------------------------------------------------------------------------- 1 | class Lifeform 2 | { 3 | public: 4 | virtual void eat() = 0; 5 | virtual void breathe() = 0; 6 | }; 7 | 8 | class Flyable 9 | { 10 | public: 11 | virtual void fly() = 0; 12 | }; 13 | 14 | class Pigeon: public Lifeform, public Flyable 15 | { 16 | public: 17 | void eat() override 18 | { 19 | // ... 20 | } 21 | 22 | void breathe() override 23 | { 24 | // ... 25 | } 26 | 27 | void fly() override 28 | { 29 | // ... 30 | } 31 | }; 32 | 33 | class Penguin: public Lifeform 34 | { 35 | public: 36 | void eat() override 37 | { 38 | // ... 39 | } 40 | 41 | void breathe() override 42 | { 43 | // ... 44 | } 45 | }; -------------------------------------------------------------------------------- /Object_Oriented_Programming/DIP_after.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | class Owner 5 | { 6 | public: 7 | virtual std::string getName() = 0; 8 | }; 9 | 10 | class Account; 11 | 12 | class Customer : public Owner 13 | { 14 | public: 15 | void setAccount(Account* account) 16 | { 17 | customerAccount = account; 18 | } 19 | virtual std::string getName() override 20 | { 21 | // return the Customer's name here... 22 | } 23 | // ... 24 | private: 25 | Account* customerAccount; 26 | // ... 27 | }; 28 | 29 | class Account 30 | { 31 | public: 32 | void setOwner(Owner* owner) 33 | { 34 | this->owner = owner; 35 | } 36 | //... 37 | private: 38 | Owner* owner; 39 | }; -------------------------------------------------------------------------------- /Object_Oriented_Programming/ISP_before.cpp: -------------------------------------------------------------------------------- 1 | class Bird 2 | { 3 | public: 4 | virtual void eat() = 0; 5 | virtual void breathe() = 0; 6 | virtual void fly() = 0; 7 | }; 8 | 9 | class Pigeon: public Bird 10 | { 11 | public: 12 | virtual void eat() override 13 | { 14 | // ... 15 | } 16 | 17 | virtual void breathe() override 18 | { 19 | // ... 20 | } 21 | 22 | virtual void fly() override 23 | { 24 | // ... 25 | } 26 | }; 27 | 28 | class Penguin: public Bird 29 | { 30 | public: 31 | virtual void eat() override 32 | { 33 | // ... 34 | } 35 | 36 | virtual void breathe() override 37 | { 38 | // ... 39 | } 40 | 41 | virtual void fly() override 42 | { 43 | // ??? 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /Object_Oriented_Programming/loose_coupling.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | class Switchable 4 | { 5 | public: 6 | virtual void on() = 0; 7 | virtual void off() = 0; 8 | }; 9 | 10 | class Switch 11 | { 12 | public: 13 | Switch(Switchable& switchable) : switchable(switchable) {} 14 | void toggle() 15 | { 16 | if (state) 17 | { 18 | state = false; 19 | switchable.off(); 20 | } 21 | else 22 | { 23 | state = true; 24 | switchable.on(); 25 | } 26 | } 27 | 28 | private: 29 | Switchable& switchable; 30 | bool state {false}; 31 | }; 32 | 33 | class Lamp: public Switchable 34 | { 35 | public: 36 | void on() override 37 | { 38 | std::cout << "Lamp is on!" << std::endl; 39 | } 40 | 41 | void off() override 42 | { 43 | std::cout << "Lamp is off!" << std::endl; 44 | } 45 | }; 46 | 47 | class Fan: public Switchable 48 | { 49 | public: 50 | void on() override 51 | { 52 | std::cout << "Fan is on!" << std::endl; 53 | } 54 | 55 | void off() override 56 | { 57 | std::cout << "Fan is off!" << std::endl; 58 | } 59 | }; 60 | 61 | int main() 62 | { 63 | Lamp lamp; 64 | Switch switch1(lamp); 65 | switch1.toggle(); 66 | switch1.toggle(); 67 | 68 | Fan fan; 69 | Switch switch2(fan); 70 | switch2.toggle(); 71 | switch2.toggle(); 72 | } -------------------------------------------------------------------------------- /Object_Oriented_Programming/Bridge_before.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | // 单维度继承设计 4 | 5 | class IMilkTea // 通用接口 6 | { 7 | virtual void order() = 0; 8 | }; 9 | 10 | class MilkTeaSmallCup: public IMilkTea 11 | { 12 | void order() override 13 | { 14 | std::cout << "order info:" << std::endl; 15 | std::cout << "size: small cup" << std::endl; 16 | } 17 | }; 18 | 19 | class MilkTeaMediumCup: public IMilkTea 20 | { 21 | void order() override 22 | { 23 | std::cout << "order info:" << std::endl; 24 | std::cout << "size: medium cup" << std::endl; 25 | } 26 | }; 27 | 28 | class MilkTeaLargeCup: public IMilkTea 29 | { 30 | void order() override 31 | { 32 | std::cout << "order info:" << std::endl; 33 | std::cout << "size: large cup" << std::endl; 34 | } 35 | }; 36 | 37 | // 多维度继承设计 38 | 39 | class MilkTeaSmallCupFairyGrass: public IMilkTea 40 | { 41 | void order() override 42 | { 43 | std::cout << "order info:" << std::endl; 44 | std::cout << "size: small cup" << std::endl; 45 | std::cout << "flavor: fairy grass" << std::endl; 46 | } 47 | }; 48 | 49 | class MilkTeaSmallCupPearl: public IMilkTea 50 | { 51 | void order() override 52 | { 53 | std::cout << "order info:" << std::endl; 54 | std::cout << "size: small cup" << std::endl; 55 | std::cout << "flavor: pearl" << std::endl; 56 | } 57 | }; 58 | 59 | // ... -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "any": "cpp", 4 | "array": "cpp", 5 | "atomic": "cpp", 6 | "*.tcc": "cpp", 7 | "cctype": "cpp", 8 | "chrono": "cpp", 9 | "clocale": "cpp", 10 | "cmath": "cpp", 11 | "condition_variable": "cpp", 12 | "cstdarg": "cpp", 13 | "cstddef": "cpp", 14 | "cstdint": "cpp", 15 | "cstdio": "cpp", 16 | "cstdlib": "cpp", 17 | "cstring": "cpp", 18 | "ctime": "cpp", 19 | "cwchar": "cpp", 20 | "cwctype": "cpp", 21 | "deque": "cpp", 22 | "unordered_map": "cpp", 23 | "vector": "cpp", 24 | "exception": "cpp", 25 | "algorithm": "cpp", 26 | "functional": "cpp", 27 | "iterator": "cpp", 28 | "map": "cpp", 29 | "memory": "cpp", 30 | "memory_resource": "cpp", 31 | "numeric": "cpp", 32 | "optional": "cpp", 33 | "random": "cpp", 34 | "ratio": "cpp", 35 | "set": "cpp", 36 | "string": "cpp", 37 | "string_view": "cpp", 38 | "system_error": "cpp", 39 | "tuple": "cpp", 40 | "type_traits": "cpp", 41 | "utility": "cpp", 42 | "fstream": "cpp", 43 | "initializer_list": "cpp", 44 | "iomanip": "cpp", 45 | "iosfwd": "cpp", 46 | "iostream": "cpp", 47 | "istream": "cpp", 48 | "limits": "cpp", 49 | "mutex": "cpp", 50 | "new": "cpp", 51 | "ostream": "cpp", 52 | "sstream": "cpp", 53 | "stdexcept": "cpp", 54 | "streambuf": "cpp", 55 | "thread": "cpp", 56 | "typeinfo": "cpp", 57 | "variant": "cpp", 58 | "bit": "cpp" 59 | } 60 | } -------------------------------------------------------------------------------- /Object_Oriented_Programming/Bridge_after.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | // 实现化部分 5 | class IMilkTeaFlavorBase 6 | { 7 | public: 8 | virtual void GetFlavor() = 0; 9 | }; 10 | 11 | class MilkTeaPearl: public IMilkTeaFlavorBase 12 | { 13 | public: 14 | void GetFlavor() override 15 | { 16 | std::cout << "flavor: pearl" << std::endl; 17 | } 18 | }; 19 | 20 | class MilkTeaFairyGrass: public IMilkTeaFlavorBase 21 | { 22 | public: 23 | void GetFlavor() override 24 | { 25 | std::cout << "flavor: fairy grass" << std::endl; 26 | } 27 | }; 28 | 29 | // 抽象化部分 30 | class IMilkTeaSizeBase 31 | { 32 | public: 33 | virtual void SetFlavor(std::shared_ptr flavorBase) 34 | { 35 | this->flavorBase = flavorBase; 36 | } 37 | virtual void Order() = 0; 38 | protected: 39 | std::shared_ptr flavorBase; 40 | }; 41 | 42 | class MilkTeaSmall: public IMilkTeaSizeBase 43 | { 44 | public: 45 | void Order() override 46 | { 47 | std::cout << "size: small" << std::endl; 48 | flavorBase->GetFlavor(); 49 | } 50 | }; 51 | 52 | class MilkTeaMedium: public IMilkTeaSizeBase 53 | { 54 | public: 55 | void Order() override 56 | { 57 | std::cout << "size: medium" << std::endl; 58 | flavorBase->GetFlavor(); 59 | } 60 | }; 61 | 62 | class MilkTeaLarge: public IMilkTeaSizeBase 63 | { 64 | public: 65 | void Order() override 66 | { 67 | std::cout << "size: large" << std::endl; 68 | flavorBase->GetFlavor(); 69 | } 70 | }; 71 | 72 | int main() 73 | { 74 | // 大杯烧仙草 75 | std::shared_ptr milkTeaFairyGrass = std::make_shared(); 76 | std::shared_ptr milkTeaLargeWithFairyGrass = std::make_shared(); 77 | milkTeaLargeWithFairyGrass->SetFlavor(milkTeaFairyGrass); 78 | milkTeaLargeWithFairyGrass->Order(); 79 | } 80 | -------------------------------------------------------------------------------- /Compile/README.md: -------------------------------------------------------------------------------- 1 | # 编译、链接、静态库、动态库 2 | 3 | ## 目录 4 | 5 | [TOC] 6 | 7 | ## 前言 8 | 9 | 在程设课上,我们运行一个C++程序的步骤通常是这样的:打开Visual Studio 2008,在文件中写好程序,然后点击“开始调试”或者“开始执行(不调试)”,一个黑色的方框就会弹出来。 10 | 11 | 实际上,从C++源代码文件到可执行文件的过程是十分复杂的,Visual Studio等现代化的IDE(Integrated Development Environment,集成开发环境)掩盖了程序构建的复杂流程。本节我们就以linux平台上的C++程序为例,简略介绍C++工程中的一些概念。 12 | 13 | **important!!** 为了获得更好的实验体验,建议大家使用linux操作系统(虚拟机或WSL)来运行本节的程序。 14 | 15 | ## 从.cpp到.exe —— C/C++程序的构建过程 16 | 17 | C/C++程序生成一个可执行文件的过程可以分为4个步骤:**预处理(Preprocessing)**、**编译(Compiling)**、**汇编(Assembly)**和**链接(Linking)**。 18 | 19 | * 预处理:在编译器处理程序之前完成头文件的包含,宏扩展,条件编译,行控制等操作。 20 | * 编译:通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将源文件代码翻译成等价的汇编代码。 21 | * 汇编:将汇编语言代码翻译成目标机器指令,生成目标文件。 22 | * 链接:将有关联的目标文件(以及库)相组合为一个可执行文件。 23 | 24 | 接下来,我们将通过演示实例介绍每一步发生的故事。 25 | 26 | 27 | ### 编译工具 28 | 29 | 针对不同的应用场景和平台,各大厂家设计了不同的C++编译工具。 30 | 31 | * MSVC(Microsoft Visual C++):MSVC是微软公司开发的C++开发工具,我们程设课上使用的Visual Studio就内置了MSVC。 32 | 33 | * GCC(GNU Compiler Collection):GCC是由GNU(GNU's Not Unix)开发的一套编译工具,支持C、C++、Fortran、Go等一系列语言。本教程中我们使用的编译工具就是GCC。 34 | 35 | GCC提供给用户的前端程序为`gcc`(针对C)和`g++`(针对C++)。它们的区别详见[gcc vs g++](https://stackoverflow.com/questions/172587/what-is-the-difference-between-g-and-gcc)。 36 | 37 | 在linux(Ubuntu)平台上,可以使用以下指令安装上述工具: 38 | 39 | ```bash 40 | $ sudo apt-get install gcc g++ 41 | ``` 42 | * 此外还有Clang、NVCC等编译工具。不同的编译工具对C++的支持不尽然相同,此处不再赘述。 43 | 44 | ### 1 预处理⭐ 45 | 46 | C++程序在预处理阶段会执行以下操作:宏的替换、头文件的插入、删除条件编译中不满足条件的部分。 47 | 48 | ```bash 49 | $ g++ –E invsqrt.cpp –o invsqrt.i 50 | ``` 51 | 52 | ### 2 编译⭐ 53 | 54 | C++程序在编译阶段会将C++文件转换为**汇编文件**。 55 | 56 | ```bash 57 | # from .i file 58 | $ g++ –S invsqrt.i –o invsqrt.s 59 | # from .cpp file 60 | $ g++ –S invsqrt.cpp –o invsqrt.s 61 | ``` 62 | 63 | ### 3 汇编⭐ 64 | 汇编语言文件经过汇编,生成**目标文件**.o文件(二进制文件,机器码),每一个源文件都对应一个目标文件。 65 | 66 | ```bash 67 | # from .s file 68 | $ g++ –c invsqrt.s –o invsqrt.o 69 | # from .cpp file 70 | $ g++ –c invsqrt.cpp –o invsqrt.o 71 | $ g++ -c main.cpp -o main.o 72 | ``` 73 | 74 | > 生成的`invsqrt.o`和`main.o`文件不能直接打开,你可以使用`readelf -a `阅读其信息。 75 | 76 | ### 4 链接⭐ 77 | 将每个源文件对应的目标.o文件链接起来,就生成一个**可执行程序文件**。 78 | 79 | ```bash 80 | $ g++ invsqrt.o main.o -o main.exe 81 | ``` 82 | 83 | 当然,如果想要使用.cpp文件一步到位生成可执行文件,可以使用以下指令: 84 | ```bash 85 | $ g++ invsqrt.cpp main.cpp -o main.exe 86 | ``` 87 | 88 | > 实际上在linux系统上,可执行文件一般是没有后缀名的。此处为了方便说明添加了`.exe`文件。 89 | 90 | > **语法总结** 91 | > 92 | > `g++`和`gcc`工具中使用的一些命令行参数: 93 | > 94 | > * `-E` 只进行预处理 95 | > 96 | > * `-S` 只进行编译 97 | > 98 | > * `-c` 只生成目标文件 99 | > 100 | > * `-o ` 指定输出文件的名称。我们约定:`.i`为预处理后的文件,`.s`为汇编文件,`.o`为目标文件。 101 | 102 | 103 | ### 静态库和动态库⭐ 104 | 105 | 出于便于复用、封装细节或防止源码泄露等原因,在实际应用过程中,我们需要把C++源码封装为库(library)。 106 | 107 | 根据其行为不同,可以将库分为静态库(static library)和动态库(shared library)。 108 | 109 | #### 静态库 110 | 111 | **静态库**的代码在编译的过程中,会被直接载入到可执行文件中。这样做的好处是:可执行文件在执行时,不再需要静态库本身。但缺点也显而易见:生成的可执行文件的体积会比较大。 112 | 113 | linux平台下静态库的后缀通常为`.a`,命名方式通常为`libxxx.a`;windows平台下静态库的后缀通常为`.lib`。 114 | 115 | 在linux平台上生成静态库,并使用动态库链接形成可执行文件的方法为: 116 | 117 | ```bash 118 | # generate static lib 119 | $ ar crv libinvsqrt.a invsqrt.o 120 | # link to generate the executable file 121 | $ g++ -static main.cpp -L . -linvsqrt -o main_shared.exe 122 | ``` 123 | 124 | #### 动态库 125 | 126 | **动态库**在程序编译时并不会被连接到目标代码中,而是在程序运行是才被载入。这就带来了一个明显的好处:不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例,减小了各个模块之间的耦合程度,也减小了可执行文件的体积。然而,这也要求用户的电脑上需要同时拥有可执行文件和动态库,也有可能因为版本不匹配等问题发生[DLL Hell](https://en.wikipedia.org/wiki/DLL_Hell)等问题。 127 | 128 | linux平台下静态库的后缀通常为`.so`,命名方式通常为`libxxx.so`;windows平台下静态库的后缀通常为`.dll`。 129 | 130 | 在linux平台上生成动态库,并使用动态库链接形成可执行文件的方法为: 131 | 132 | ```bash 133 | # generate shared lib 134 | $ g++ invsqrt.cpp -I ./ -fPIC -shared -o libinvsqrt.so 135 | # move the shared library to system 136 | $ sudo mv libinvsqrt.so /usr/local/lib 137 | # refresh 138 | $ sudo ldconfig 139 | # link to generate the executable file 140 | $ g++ main.cpp -L . -linvsqrt -o main_shared.exe 141 | ``` 142 | 143 | ## 写在最后 144 | 145 | 由于时间所限,还有很多有趣的内容我们没有涉及: 146 | 147 | * gcc/g++有着丰富的命令行参数设置,比如程序优化、C/C++语言标准设置等。 148 | * 在本节中,我们只介绍了如何在linux平台上生成和使用静态库、动态库。实际上,利用Visual Studio也可以便捷地在windows平台上生成静态库、动态库。 149 | * ... 150 | 151 | 略过上述内容不会对我们的教学产生太大影响。感兴趣的同学可以参考以下文档: 152 | 153 | * [GCC官网](https://gcc.gnu.org/) 154 | * [learn cpp](https://www.learncpp.com/) 一份新手友好的C++入门文档。 155 | * [演练:使用Visual Studio创建并使用静态库](https://docs.microsoft.com/zh-cn/cpp/build/walkthrough-creating-and-using-a-static-library-cpp?view=msvc-170) 156 | * [演练:使用Visual Studio创建并使用动态库](https://docs.microsoft.com/zh-cn/cpp/build/walkthrough-creating-and-using-a-dynamic-link-library-cpp?view=msvc-170) 157 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | # BasedOnStyle: Google 4 | AccessModifierOffset: -1 5 | AlignAfterOpenBracket: Align 6 | AlignConsecutiveMacros: false 7 | AlignConsecutiveAssignments: false 8 | AlignConsecutiveDeclarations: false 9 | AlignEscapedNewlines: Left 10 | AlignOperands: true 11 | AlignTrailingComments: true 12 | AllowAllArgumentsOnNextLine: true 13 | AllowAllConstructorInitializersOnNextLine: true 14 | AllowAllParametersOfDeclarationOnNextLine: true 15 | AllowShortBlocksOnASingleLine: Never 16 | AllowShortCaseLabelsOnASingleLine: false 17 | AllowShortFunctionsOnASingleLine: All 18 | AllowShortLambdasOnASingleLine: All 19 | AllowShortIfStatementsOnASingleLine: WithoutElse 20 | AllowShortLoopsOnASingleLine: true 21 | AlwaysBreakAfterDefinitionReturnType: None 22 | AlwaysBreakAfterReturnType: None 23 | AlwaysBreakBeforeMultilineStrings: true 24 | AlwaysBreakTemplateDeclarations: Yes 25 | BinPackArguments: true 26 | BinPackParameters: true 27 | BraceWrapping: 28 | AfterCaseLabel: false 29 | AfterClass: false 30 | AfterControlStatement: false 31 | AfterEnum: false 32 | AfterFunction: false 33 | AfterNamespace: false 34 | AfterObjCDeclaration: false 35 | AfterStruct: false 36 | AfterUnion: false 37 | AfterExternBlock: false 38 | BeforeCatch: false 39 | BeforeElse: false 40 | IndentBraces: false 41 | SplitEmptyFunction: true 42 | SplitEmptyRecord: true 43 | SplitEmptyNamespace: true 44 | BreakBeforeBinaryOperators: None 45 | BreakBeforeBraces: Attach 46 | BreakBeforeInheritanceComma: false 47 | BreakInheritanceList: BeforeColon 48 | BreakBeforeTernaryOperators: true 49 | BreakConstructorInitializersBeforeComma: false 50 | BreakConstructorInitializers: BeforeColon 51 | BreakAfterJavaFieldAnnotations: false 52 | BreakStringLiterals: true 53 | ColumnLimit: 80 54 | CommentPragmas: '^ IWYU pragma:' 55 | CompactNamespaces: false 56 | ConstructorInitializerAllOnOneLineOrOnePerLine: true 57 | ConstructorInitializerIndentWidth: 4 58 | ContinuationIndentWidth: 4 59 | Cpp11BracedListStyle: true 60 | DeriveLineEnding: true 61 | DerivePointerAlignment: true 62 | DisableFormat: false 63 | ExperimentalAutoDetectBinPacking: false 64 | FixNamespaceComments: true 65 | ForEachMacros: 66 | - foreach 67 | - Q_FOREACH 68 | - BOOST_FOREACH 69 | IncludeBlocks: Regroup 70 | IncludeCategories: 71 | - Regex: '^' 72 | Priority: 2 73 | SortPriority: 0 74 | - Regex: '^<.*\.h>' 75 | Priority: 1 76 | SortPriority: 0 77 | - Regex: '^<.*' 78 | Priority: 2 79 | SortPriority: 0 80 | - Regex: '.*' 81 | Priority: 3 82 | SortPriority: 0 83 | IncludeIsMainRegex: '([-_](test|unittest))?$' 84 | IncludeIsMainSourceRegex: '' 85 | IndentCaseLabels: true 86 | IndentGotoLabels: true 87 | IndentPPDirectives: None 88 | IndentWidth: 2 89 | IndentWrappedFunctionNames: false 90 | JavaScriptQuotes: Leave 91 | JavaScriptWrapImports: true 92 | KeepEmptyLinesAtTheStartOfBlocks: false 93 | MacroBlockBegin: '' 94 | MacroBlockEnd: '' 95 | MaxEmptyLinesToKeep: 1 96 | NamespaceIndentation: None 97 | ObjCBinPackProtocolList: Never 98 | ObjCBlockIndentWidth: 2 99 | ObjCSpaceAfterProperty: false 100 | ObjCSpaceBeforeProtocolList: true 101 | PenaltyBreakAssignment: 2 102 | PenaltyBreakBeforeFirstCallParameter: 1 103 | PenaltyBreakComment: 300 104 | PenaltyBreakFirstLessLess: 120 105 | PenaltyBreakString: 1000 106 | PenaltyBreakTemplateDeclaration: 10 107 | PenaltyExcessCharacter: 1000000 108 | PenaltyReturnTypeOnItsOwnLine: 200 109 | PointerAlignment: Left 110 | RawStringFormats: 111 | - Language: Cpp 112 | Delimiters: 113 | - cc 114 | - CC 115 | - cpp 116 | - Cpp 117 | - CPP 118 | - 'c++' 119 | - 'C++' 120 | CanonicalDelimiter: '' 121 | BasedOnStyle: google 122 | - Language: TextProto 123 | Delimiters: 124 | - pb 125 | - PB 126 | - proto 127 | - PROTO 128 | EnclosingFunctions: 129 | - EqualsProto 130 | - EquivToProto 131 | - PARSE_PARTIAL_TEXT_PROTO 132 | - PARSE_TEST_PROTO 133 | - PARSE_TEXT_PROTO 134 | - ParseTextOrDie 135 | - ParseTextProtoOrDie 136 | CanonicalDelimiter: '' 137 | BasedOnStyle: google 138 | ReflowComments: true 139 | SortIncludes: true 140 | SortUsingDeclarations: true 141 | SpaceAfterCStyleCast: false 142 | SpaceAfterLogicalNot: false 143 | SpaceAfterTemplateKeyword: true 144 | SpaceBeforeAssignmentOperators: true 145 | SpaceBeforeCpp11BracedList: false 146 | SpaceBeforeCtorInitializerColon: true 147 | SpaceBeforeInheritanceColon: true 148 | SpaceBeforeParens: ControlStatements 149 | SpaceBeforeRangeBasedForLoopColon: true 150 | SpaceInEmptyBlock: false 151 | SpaceInEmptyParentheses: false 152 | SpacesBeforeTrailingComments: 2 153 | SpacesInAngles: false 154 | SpacesInConditionalStatement: false 155 | SpacesInContainerLiterals: true 156 | SpacesInCStyleCastParentheses: false 157 | SpacesInParentheses: false 158 | SpacesInSquareBrackets: false 159 | SpaceBeforeSquareBrackets: false 160 | Standard: Auto 161 | StatementMacros: 162 | - Q_UNUSED 163 | - QT_REQUIRE_VERSION 164 | TabWidth: 8 165 | UseCRLF: false 166 | UseTab: Never 167 | ... 168 | 169 | -------------------------------------------------------------------------------- /Multi-file_Programming/README.md: -------------------------------------------------------------------------------- 1 | # C++多文件编程 2 | 3 | 鸟x 4 | 5 | ## 目录 6 | 7 | [TOC] 8 | 9 | ## 从单文件到多文件 10 | 11 | 首先来看一段写在单文件里的C++程序: 12 | 13 | ```C++ 14 | #include 15 | 16 | // int add(int x, int y); 17 | 18 | int main() 19 | { 20 | std::cout << "The sum of 3 and 4 is: " << add(3, 4) << '\n'; 21 | return 0; 22 | } 23 | 24 | int add(int x, int y) 25 | { 26 | return x + y; 27 | } 28 | ``` 29 | 30 | 显然,这个程序会引发编译器报错,当编译器看到主函数第一行的标识符add时,并不知道它是什么(因为我们在主函数后才定义它)。我们要么使用前向声明,要么调整代码顺序。 31 | 32 | 现在,我们来看一个与之相似的多文件程序: 33 | 34 | ```cpp 35 | // add.cpp 36 | int add(int x, int y) 37 | { 38 | return x + y; 39 | } 40 | ``` 41 | 42 | ```cpp 43 | // main.cpp 44 | #include 45 | 46 | int main() 47 | { 48 | std::cout << "The sum of 3 and 4 is: " << add(3, 4) << '\n'; // compile error 49 | return 0; 50 | } 51 | ``` 52 | 53 | 系统由两个cpp源文件构成。编译时,编译器要么先编译add.cpp,要么先编译main.cpp,无论是哪一种情况都会引发报错,同样因为编译器不知道主函数的add标识符是什么意思。(注意,编译器编译某一文件时,既不会知晓其他文件的内容,也不会记住曾经编译过的文件的内容) 54 | 55 | ```cpp 56 | // MSVC 57 | error C3861: 'add': identifier not found 58 | // GCC 59 | error: 'add' was not declared in this scope 60 | ``` 61 | 62 | 我们的解决方案仍然与之前一样,然而,由于add在另一个文件里,调整代码顺序是不可能的,我们只能求助于前向声明: 63 | 64 | ```cpp 65 | #include 66 | 67 | int add(int x, int y); // needed so main.cpp knows that add() is a function defined elsewhere 68 | 69 | int main() 70 | { 71 | std::cout << "The sum of 3 and 4 is: " << add(3, 4) << '\n'; 72 | return 0; 73 | } 74 | ``` 75 | 76 | 现在,编译器就知道add是一个函数,不会再报错。接下来就由链接器将主函数对add的调用与add的定义连接。如果发现add并没有被定义,就会引发链接器报错。 77 | 78 | ```cpp 79 | // MSVC 80 | error LNK2019: unresolved external symbol "int __cdecl add(int,int)" (?add@@YAHHH@Z) referenced in function _main 81 | // GCC 82 | undefined reference to `add(int, int)' 83 | ``` 84 | 85 | ## 头文件 86 | 87 | 随着程序越来越大(并使用更多的文件),对想使用的定义在不同文件中的每个函数进行前向声明变得越来越乏味。如果你能把所有的前向声明放在一个地方,然后在需要的时候导入它们,那不是很好吗? 88 | 89 | C++源文件.cpp并不是C++程序中唯一常见的文件。另一种类型的文件被称为头文件。头文件的扩展名通常是.h,但你偶尔也会看到它们的扩展名是.hpp,或者根本就没有扩展名。头文件的主要目的是将声明传播到代码文件,允许我们把声明放在一个地方而在我们需要的时候导入它们。 90 | 91 | 最常见的例子,`#include `中的iostream就是头文件,包含它在内的一系列头文件从属于标准库,`std::cout`就是在标准库中声明的。 92 | 93 | 当涉及到函数和变量时,值得注意的是,**头文件通常只包含函数和变量的声明**,而不是函数和变量的定义(否则可能导致违反单一定义规则,见下)。 `std::cout`在iostream头文件中是向前声明的,但定义为C++标准库的一部分,它在链接器阶段会自动链接到你的程序中。 94 | 95 | ### 为什么头文件不包含定义 96 | 97 | 来看这个例子: 98 | 99 | ```C++ 100 | // a.h 101 | int a = 5;// defination 102 | ``` 103 | 104 | ```c++ 105 | // a.cpp 106 | #include "a.h" 107 | ``` 108 | 109 | ```cpp 110 | // main.cpp 111 | #include "a.h" 112 | #include 113 | 114 | int main() 115 | { 116 | std::cout << a; 117 | return 0; 118 | } 119 | ``` 120 | 121 | 此时会引发链接错误。原因在于,a.cpp与main.cpp都包含了a.h,于是两个源文件都含有a的定义,导致重定义错误。在多文件编程中,多个源文件包含一个头文件的情况是很常见的,如果在头文件中进行定义,就不免会发生上面的情况。 122 | 123 | ### 自定义头文件示例 124 | 125 | 以前面的add为例,演示如何写自己的头文件并使用。 126 | 127 | 一般而言,头文件会与对应的源文件对应(显而易见,因为头文件里是声明,而源文件里是对应的定义)。于是我们创建add.h与add.cpp如下: 128 | 129 | ```cpp 130 | // 1) We really should have a header guard here, but will omit it for simplicity (we'll cover header guards below) 131 | 132 | // 2) This is the content of the .h file, which is where the declarations go 133 | int add(int x, int y); // function prototype for add.h -- don't forget the semicolon! 134 | ``` 135 | 136 | ```cpp 137 | #include "add.h" // Insert contents of add.h at this point. Note use of double quotes here. 138 | 139 | int add(int x, int y) 140 | { 141 | return x + y; 142 | } 143 | ``` 144 | 145 | 现在,我们就可以在main.cpp中使用add函数: 146 | 147 | ```cpp 148 | #include "add.h" // Insert contents of add.h at this point. Note use of double quotes here. 149 | #include 150 | 151 | int main() 152 | { 153 | std::cout << "The sum of 3 and 4 is " << add(3, 4) << '\n'; 154 | return 0; 155 | } 156 | ``` 157 | 158 | ## 多文件编程中务必牢记的书写规范 159 | 160 | ### 不要再写using namespace std了! 161 | 162 | namespace std是标准命名空间,其中包括了cout、cin、max、min等一系列很有用处的函数。根据命名空间的使用规则(::),我们可以写出这样的代码: 163 | 164 | ```cpp 165 | #include 166 | 167 | int main() 168 | { 169 | std::cout << "Hello world!"; // when we say cout, we mean the cout defined in the std namespace 170 | return 0; 171 | } 172 | ``` 173 | 174 | using的意义是,在整个文件中对应的函数名都默认在其后的命名空间中,using namespace std就是在整个文件范围内指定使用标准命名空间(如果不存在歧义): 175 | 176 | ```cpp 177 | #include 178 | 179 | using namespace std; // this is a using directive that allows us to access names in the std namespace with no namespace prefix 180 | 181 | int main() 182 | { 183 | cout << "Hello world!"; 184 | return 0; 185 | } 186 | ``` 187 | 188 | 当然,这能帮我们少做很多琐碎活,如果你可以确保不会使用来自其他命名空间的同名函数,就可以大胆使用using(比如OJ题,它们通常只要求在一个文件完成所有内容)。但除此之外的情况,为了尽可能降低bug出现的概率,不要使用using namespace std!来看这个例子: 189 | 190 | ```cpp 191 | #include // imports the declaration of std::cout 192 | 193 | using namespace std; // makes std::cout accessible as "cout" 194 | 195 | int cout() // defines our own "cout" function in the global namespace 196 | { 197 | return 5; 198 | } 199 | 200 | int main() 201 | { 202 | cout << "Hello, world!"; // Compile error! Which cout do we want here? The one in the std namespace or the one we defined above? 203 | 204 | return 0; 205 | } 206 | ``` 207 | 208 | 现在,我们本打算使用自己定义的cout函数(这看起来很蠢,但别忘了std中还有min、max等经常被用来自己定义函数的函数名字),但由于我们using namespace std,编译器不能分辨我们究竟要使用哪个命名空间的cout,于是就会报错。在多文件编程中,这种错误的出现会让人摸不着头脑,难以定位错误的所在。 209 | 210 | ### 源文件应导入它们对应的头文件(如有) 211 | 212 | 这样做允许编译器在编译期就能发现错误,而非到链接期才能发现。 213 | 214 | ```cpp 215 | // a.h 216 | int something(int); // return type of forward declaration is int 217 | ``` 218 | 219 | ```cpp 220 | // a.cpp 221 | #include "a.h" 222 | 223 | void something(int) // error: wrong return type 224 | { 225 | } 226 | ``` 227 | 228 | 由于源文件导入了头文件,返回值类型不匹配的错误在编译期即可发现(很多IDE的语法检查也可以发现),方便我们debug。 229 | 230 | ### 使用双引号来导入自定义的头文件 231 | 232 | 使用尖括号时,它告诉预处理器这不是我们自定义的头文件。编译器将只在包含目录(include directories)所指定的目录中搜索头文件。包含目录是作为项目/IDE设置/编译器设置的一部分来配置的,通常默认为编译器和/或操作系统所带头文件的目录。编译器不会在你项目的源代码目录中搜索头文件。 233 | 234 | 使用双引号时,我们告诉预处理器这是我们自定义的头文件。编译器将首先在当前目录下搜索头文件。如果在那里找不到匹配的头文件,它就会在包含目录中搜索。 235 | 236 | 因此,使用双引号来包含你编写的头文件,或者预期在当前目录中可以找到的头文件。使用尖括号来包含编译器、操作系统或安装的第三方库所附带的头文件。 237 | 238 | ### 显式包含所需的所有头文件 239 | 240 | **每个文件都应该明确地#include它所需要的所有头文件来进行编译。不要依赖从其他头文件中转来的头文件。** 241 | 242 | 如果你包含了a.h,而a.h中包含了b.h,则b.h为隐式包含,a.h为显式包含。你不应该依赖通过这种方式包含的b.h的内容,因为头文件的实现可能会随着时间的推移而改变,或者在不同的系统中是不同的。而这一旦发生,你的代码就可能不能在别的系统上编译,或者将来不能编译。直接同时显式包含a.h和b.h就能解决这个问题。 243 | 244 | ### 给自定义头文件加上header guard 245 | 246 | 前面提到,如果在一个文件中多次定义一个变量/函数会引发重定义报错。现在来看这个例子: 247 | 248 | ```cpp 249 | // square.h 250 | // We shouldn't be including function definitions in header files 251 | // But for the sake of this example, we will 252 | int getSquareSides() 253 | { 254 | return 4; 255 | } 256 | ``` 257 | 258 | ```cpp 259 | // geometry.h 260 | #include "square.h" 261 | ``` 262 | 263 | ```cpp 264 | // main.cpp 265 | #include "square.h" 266 | #include "geometry.h" 267 | 268 | int main() 269 | { 270 | return 0; 271 | } 272 | ``` 273 | 274 | 则在main.cpp中,getSquareSides通过两次包含被定义了两次,将引发重定义问题。关键的问题在于,只站在main.cpp的角度来看,它很无辜,只不过是普通地包含了两个不同的头文件而已。因此,解决这个问题应该从头文件本身下手。这就是我们需要header guard的原因。 275 | 276 | header guard的格式如下: 277 | 278 | ```cpp 279 | #ifndef SOME_UNIQUE_NAME_HERE 280 | #define SOME_UNIQUE_NAME_HERE 281 | 282 | // your declarations (and certain types of definitions) here 283 | 284 | #endif 285 | ``` 286 | 287 | 这样,就可以保证相同的部分只会被包含一次。 288 | 289 | 现代编译器往往还会提供另一种更简洁的header guard——`#pragma once`。 290 | 291 | ```cpp 292 | #pragma once 293 | 294 | // your code here 295 | ``` 296 | 297 | 它的效果与传统的header guard相同,而且更加简单。但是我们仍然建议使用传统的header guard,因为#pragma once在某些编译器上并不受支持。 298 | 299 | ### 严禁循环包含 300 | 301 | 我们来看这样一个例子: 302 | 303 | ```CPP 304 | // in b.h 305 | #include "a.h" 306 | // ... 307 | 308 | 309 | // in a.h 310 | #include "b.h" 311 | // ... 312 | ``` 313 | 314 | 未使用header guard时,这样的写法会引发连锁反应:b.h包含a.h,后者包含b.h,再包含a.h...... 315 | 316 | 即使使用了header guard消除了这种连锁反应,也会出现错误——如果b包含a,那么a包含b的代码则会因为header guard在预处理阶段就失去作用,于是a没能成功包含b,则a无法看到其需要的b中的声明。 317 | 318 | 因此,禁止循环包含!(此外, 出现循环包含时有很大的可能就是你的代码设计不好,例如出现了循环依赖,这部分讨论见oop依赖反转原则) 319 | 320 | ## Reference 321 | 322 | [Learn C++ – Skill up with our free tutorials (learncpp.com)](https://www.learncpp.com/),2-8至2-13 323 | 324 | ## 特别鸣谢 325 | 326 | 刘雪枫学长通读了讲义初版,纠正了其中若干错误。 327 | -------------------------------------------------------------------------------- /Compile_ToolChains/README.md: -------------------------------------------------------------------------------- 1 | # Makefile & CMake 2 | 3 | ## 目录 4 | 5 | [TOC] 6 | 7 | ## 前言 8 | 9 | 想象一下我们有如下C++程序`hello.cpp`: 10 | 11 | ```cpp 12 | #include 13 | int main() 14 | { 15 | std::cout << "hello world!" << std::endl; 16 | } 17 | ``` 18 | 19 | 我们需要在终端输入以下指令: 20 | 21 | ```bash 22 | $ g++ hello.cpp -o hello 23 | ``` 24 | 25 | 这时我们就可以生成可执行文件`hello`。但在实际应用场景中,我们可能会面临如下问题: 26 | 27 | * 项目中的`.h`文件和`.cpp`文件十分繁多。 28 | * 各`.h`文件、`.cpp`文件的依赖关系十分复杂。 29 | * 多文件可能会出现重复编译的情况,拖慢编译速度。 30 | * ... 31 | 32 | 为了解决这些问题,`makefile`和`CMake`应运而生。 33 | 34 | ## Makefile 35 | 36 | 在linux(Ubuntu)平台上,`make`工具可以通过以下方式安装: 37 | 38 | ```bash 39 | $ sudo apt-get install make 40 | ``` 41 | 42 | makefile文件描述了C/C++工程的编译规则,可以用来指明源文件的编译顺序、依赖关系、是否需要重新编译等,自动化编译C/C++项目(实际上也不止局限于C/C++项目)。 43 | 44 | 我们可以考虑以下实例: 45 | 46 | ``` 47 | . 48 | ├── invsqrt.cpp 49 | ├── invsqrt.h 50 | ├── main.cpp 51 | └── makefile 52 | ``` 53 | 54 | makefile如下: 55 | 56 | ```makefile 57 | CXXFLAGS = -std=c++17 -O2 58 | 59 | main: main.o invsqrt.o 60 | $(CXX) $(CXXFLAGS) -o $@ $^ 61 | 62 | main.o: main.cpp invsqrt.h 63 | $(CXX) $(CXXFLAGS) -o $@ -c $< 64 | 65 | invsqrt.o: invsqrt.cpp invsqrt.h 66 | $(CXX) $(CXXFLAGS) -o $@ -c $< 67 | 68 | .PHONY: clean 69 | clean: 70 | rm main.o invsqrt.o main 71 | ``` 72 | 73 | 此处,我们不需要理解每一段代码的具体含义——由于makefile文件的可读性较差,在日后的开发工作中,我们并不需要直接编写makefile(之后我们可以看到,我们可以直接通过CMake工具生成makefile)。 74 | 75 | 当然,目前仍然有许多开源项目使用makefile构建程序,因此了解如何使用makefile仍然是十分有必要的。大部分开源项目中的makefile支持以下指令: 76 | 77 | * 在makefile的同目录下输入`make`,就可以按照makefile所指定的编译规则自动编译整个工程。 78 | * 在makefile的同目录下输入`make clean`,可以删除编译生成的中间文件(如`.o`文件等)和可执行文件。 79 | * 在makefile的同目录下输入`make install`(一般需要root权限),可以安装编译好的可执行文件(默认路径为`/usr/local/bin`,安装好后可以在命令行中直接调用)、库(默认路径为`/usr/local/lib`,安装好后可以直接链接)、头文件(默认路径为`/usr/local/include`,安装好后可以直接使用`#include `引用)。 80 | 81 | ## CMake 82 | 83 | makefile存在以下问题: 84 | * 代码可读性极差,难以维护。 85 | * 语法复杂。 86 | * 跨平台性差。比如linux平台下的makefile在windows下可能无法工作,因为linux的删除指令是`rm`,windows下的删除指令是`del`。 87 | * ... 88 | 89 | 90 | 因此,在目前的C++工程中,我们多使用[CMake](https://cmake.org/)来管理项目。CMake是一种跨平台的编译工具,可以用较为简洁易读的语法描述C++项目的编译、链接、安装过程等,在现代C++项目上得到了广泛应用。 91 | 92 | ### Linux 93 | 94 | 在linux(Ubuntu)平台上,`cmake`工具可以通过以下方式安装: 95 | 96 | ```bash 97 | $ sudo apt-get install cmake 98 | ``` 99 | 100 | 在linux平台上,cmake工具的使用一般分为两步 1) 使用`CMakeLists.txt`生成`makefile`。 2) 使用`makefile`自动化编译项目。 101 | 102 | #### 1 第一个CMake项目⭐ 103 | 104 | CMake的项目文件叫做`CMakeLists.txt`。其放置位置如下图所示: 105 | ``` 106 | ├── CMakeLists.txt 107 | └── main.cpp 108 | ``` 109 | 110 | 该项目的`CMakeLists.txt`中需要添加以下内容: 111 | ```cmake 112 | cmake_minimum_required(VERSION 3.5) 113 | project (hello_world) 114 | add_executable(hello_world main.cpp) 115 | ``` 116 | 117 | > **语法总结1** 118 | > * `cmake_minimum_required(VERSION 3.5)` CMake需要的最小版本。CMake的版本可以在命令行中输入`cmake --version`获取,一般无强制要求。 119 | > * `project()` 指定工程名称。 120 | > * `add_executable( )` 生成可执行文件。 121 | 122 | 操作方法如下: 123 | 124 | 1. 输入`cmake CMakeLists.txt`,目录下将会生成一个`Makefile`文件。 125 | 2. 输入`make`,即可将源代码编译生成可执行文件。此处将会在与`CMakeLists.txt`相同目录的位置生成一个可执行文件`hello_word`,输入`./hello_word`即可运行该可执行文件。 126 | 3. 此外,输入`make help`,你也可以查看使用当前的`Makefile`所能执行的所有指令,例如`make clean`(清楚生成的可执行文件和中间文件)。 127 | 128 | #### 2 多文件⭐ 129 | 130 | 在平时的程设小作业中,我们习惯将所有的代码都写在一个`.cpp`文件中。但在实际工程中,为了方便代码复用和运行维护,通常将所有的文件划分为头文件(`.h`),模块文件(`.cpp`)和主程序文件(`.cpp`)。 131 | 132 | 在本节中,我们将在头文件中声明一个计算平方根倒数的函数,在模块文件中实现其主体,然后在主函数中调用它。项目结构如下: 133 | 134 | ``` 135 | . 136 | ├── CMakeLists.txt 137 | ├── include 138 | │ └── invsqrt.h 139 | └── src 140 | ├── invsqrt.cpp 141 | └── main.cpp 142 | ``` 143 | 144 | > tips: 在C++工程中,我们通常在`include/`目录下放置头文件,在`src/`目录下放置源文件。 145 | 146 | 该项目的`CMakeLists.txt`中需要添加以下内容: 147 | ```cmake 148 | # build part 149 | cmake_minimum_required(VERSION 3.5) 150 | project(invsqrt) 151 | set(SOURCES src/invsqrt.cpp src/main.cpp) 152 | add_executable(invsqrt ${SOURCES}) 153 | target_include_directories(invsqrt PUBLIC ${PROJECT_SOURCE_DIR}/include) 154 | 155 | # debug part 156 | message("CMAKE_SOURCE_DIR: ${CMAKE_SOURCE_DIR}") 157 | message("PROJECT_SOURCE_DIR: ${PROJECT_SOURCE_DIR}") 158 | 159 | message("SOURCES: ${SOURCES}") 160 | ``` 161 | 162 | > **语法总结2** 163 | > * `set( )` 设置变量 164 | > * `target_include_directories( )` 指定所要包含的头文件。 165 | > * `message("your message")` 在终端打印信息。 166 | 167 | 这里需要特别说明一下CMake中的变量使用。CMake中的变量分为两种: 168 | 169 | * 显式变量:使用`set`指令定义的变量。 170 | * 隐式变量:通过其它指令隐式生成的变量。如该项目中会隐式生成`PROJECT_SOURCE_DIR`变量,默认为`CMakeLists.txt`所在的文件夹。 171 | 172 | CMake中有丰富的变量,用于定义工程目录、编译选项等,此处不做过多展开。想要了解更多,可以参考文末列出的参考文档。 173 | 174 | #### 3 静态库和动态库⭐ 175 | 176 | 有些时候,出于方便复用、防止源码泄露等原因,我们需要将代码封装为静态库和动态库。CMake同样提供了生成静态库和动态库的功能。 177 | 178 | ##### 3.1 静态库 179 | 180 | 在此处,我们将上一小节中计算平方根倒数的程序封装为静态库。项目结构如下: 181 | ``` 182 | . 183 | ├── CMakeLists.txt 184 | ├── include 185 | │ └── invsqrt.h 186 | └── src 187 | ├── invsqrt.cpp 188 | └── main.cpp 189 | ``` 190 | 191 | 该项目的`CMakeLists.txt`中需要添加以下内容: 192 | ```cmake 193 | cmake_minimum_required(VERSION 3.5) 194 | project(invsqrt) 195 | 196 | # create static library 197 | add_library(invsqrt_static STATIC src/invsqrt.cpp) 198 | target_include_directories(invsqrt_static PUBLIC ${PROJECT_SOURCE_DIR}/include) 199 | 200 | # create executable 201 | add_executable(invsqrt src/main.cpp) 202 | target_link_libraries(invsqrt PRIVATE invsqrt_static) 203 | ``` 204 | 205 | > **语法总结3** 206 | > * `add_library( STATIC )` 生成静态库 207 | > * `target_link_libraries( )` 指定所要链接的库。 208 | 209 | 此处我们使用一种更为优雅的生成方式——我们期望将生成的静态库、可执行文件输出到`build`文件夹里,而不是和主项目混杂在一起。为此我们需要输入以下指令: 210 | 211 | ```bash 212 | $ mkdir build 213 | $ cd build 214 | $ cmake .. # 使用的是上一层目录的CMakeLists.txt,因此需要输入'..' 215 | $ make 216 | ``` 217 | 218 | 我们将会在`build/`目录下看到静态库`libinvsqrt_static.a`和可执行文件`invsqrt`。 219 | 220 | ##### 3.2 动态库 221 | 222 | 项目目录结构同静态库一节。 223 | 224 | 该项目的`CMakeLists.txt`中需要添加以下内容: 225 | ```cmake 226 | cmake_minimum_required(VERSION 3.5) 227 | project(invsqrt) 228 | 229 | # create shared library 230 | add_library(invsqrt_shared SHARED src/invsqrt.cpp) 231 | target_include_directories(invsqrt_shared PUBLIC ${PROJECT_SOURCE_DIR}/include) 232 | 233 | # create executable 234 | add_executable(invsqrt src/main.cpp) 235 | target_link_libraries(invsqrt PRIVATE invsqrt_shared) 236 | 237 | ``` 238 | 239 | > **语法总结4** 240 | > * `add_library( SHARED )` 生成动态库 241 | 242 | 同样按照上小节的方法生成项目。我们将会在`build/`目录下看到动态库`libinvsqrt_shared.so`和可执行文件`invsqrt`。 243 | 244 | #### 4 使用第三方库 245 | 246 | 在实际的C++工程中,我们可能需要链接一些开源的第三方库。CMake也提供了相关的配置方式。我们以谷歌开发的单元测试框架`googletest`为例: 247 | 248 | > googletest的安装方法: 249 | > ```bash 250 | > $ git clone https://github.com/google/googletest.git 251 | > # or git clone git@github.com:google/googletest.git 252 | > $ cd googletest 253 | > $ mkdir build 254 | > $ cd build 255 | > $ cmake ../ 256 | > $ make -j all 257 | > $ make install 258 | > # or sudo make install 259 | > ``` 260 | 261 | 项目结构如下: 262 | 263 | ``` 264 | . 265 | ├── CMakeLists.txt 266 | ├── include 267 | │ └── mysqrt.h 268 | └── src 269 | ├── mysqrt.cpp 270 | └── main.cpp 271 | ``` 272 | 273 | ```cmake 274 | cmake_minimum_required(VERSION 2.6) 275 | 276 | project(cmake_with_gtest) 277 | 278 | set(SOURCES src/mysqrt.cpp src/main.cpp) 279 | 280 | find_package(GTest) 281 | message("GTEST_LIBRARIES: ${GTEST_LIBRARIES}") 282 | message("GTEST_INCLUDE_DIRS: ${GTEST_INCLUDE_DIRS}") 283 | 284 | include_directories(${GTEST_INCLUDE_DIRS} ${PROJECT_SOURCE_DIR}/include) 285 | 286 | add_executable(cmake_with_gtest ${SOURCES}) 287 | target_link_libraries(cmake_with_gtest ${GTEST_LIBRARIES} pthread) 288 | ``` 289 | 290 | > **语法总结5** 291 | > * `find_package()` 查询第三方库的位置。若查找成功,则初始化变量`_INCLUDE_DIR`(第三方库的头文件目录)以及`_LIBRARIES`(第三方库的静态/动态库目录)。 292 | 293 | CMake支持的所有第三方库可以在[https://cmake.org/cmake/help/latest/manual/cmake-modules.7.html](https://cmake.org/cmake/help/latest/manual/cmake-modules.7.html)中找到。 294 | 295 | ### windows 296 | 297 | 我们也可以在windows上使用CMake管理C++程序: 298 | 299 | 1. 在创建新项目时选择“CMake项目”。![stage 1](https://s2.loli.net/2022/07/03/fT1tYzvLpF6jUcS.png) 300 | 301 | 2. 创建新项目后,我们可以在文件夹中看到`.cpp`、`.h`和`CMakeLists.txt`模板。我们需要添加自己的头文件、源文件,并修改`CMakeLists.txt`。 302 | 303 | ![stage 2](https://s2.loli.net/2022/07/03/HeGY21a8mc6k34K.png) 304 | 305 | ```cmake 306 | # CMakeList.txt: CMakeProject1 的 CMake 项目,在此处包括源代码并定义 307 | # 项目特定的逻辑。 308 | # 309 | cmake_minimum_required (VERSION 3.8) 310 | 311 | # 将源代码添加到此项目的可执行文件。 312 | add_executable (CMakeProject1 "invsqrt.cpp" "main.cpp" "invsqrt.h" ) 313 | 314 | # TODO: 如有需要,请添加测试并安装目标。 315 | ``` 316 | 317 | 3. 配置好上述工程后,直接生成即可。 318 | 319 | ![stage 3](https://s2.loli.net/2022/07/03/qSsrgYDGZJVpO5I.png) 320 | 321 | ## 写在最后 322 | 323 | CMake还有很多强大的功能: 324 | 325 | * 设置C++工程的语言标准、编译优化选项。 326 | * 层级文件之间`CMakeLists.txt`的相互调用,以便应用于目录层级更加复杂的C++工程。 327 | * 对生成的库、可执行文件等进行安装。 328 | * ... 329 | 330 | 略过上述内容不会对我们的教学产生太大影响。感兴趣的同学可以参考以下文章: 331 | 332 | * [CMake官方文档](https://cmake.org/documentation/) 333 | * [cmake-examples](https://github.com/ttroy50/cmake-examples) 该GitHub仓库中有很多开箱即用的CMake实例。 334 | * [为什么编译c/c++要用makefile,而不是直接用shell呢?](https://www.zhihu.com/question/461953861/answer/1914452432) 这篇博文详细地阐述了使用makefile的动机和意义(\xfgg/)。 335 | * [跟我一起写Makefile](https://seisman.github.io/how-to-write-makefile/introduction.html) Makefile教程。从中大家也可以看出Makefile的语法十分不友好... 336 | -------------------------------------------------------------------------------- /Object_Oriented_Programming/README.md: -------------------------------------------------------------------------------- 1 | # 面向对象程序设计基础 2 | 3 | ## 目录 4 | 5 | [TOC] 6 | 7 | 在大一的程设课上,我们系统学习了C++的语法,掌握了一些编写小型程序的技能。实际上,要想写出一个可读性好、可复用、鲁棒性强的程序,掌握一些基本的设计原则是十分必要的。 8 | 9 | 本讲的内容并不针对具体的某一语言,而且相比之前的一些内容,本讲的知识更需要在长期的实践中“内化”;与此同时,与软件工程相关的理论博大精深,本讲仅仅挑选一些代表性的原则,只能带领大家入门,想要了解更多还需要仔细阅读文末提供的书单~ 10 | 11 | ## KISS 12 | 13 | KISS代表着“Keep It Simple and Stupid”。KISS原则指出,简单性应该是软件开发的主要目标,应该避免不必要的复杂性。 14 | 15 | 不过,如何界定“简单”?KISS原则指出,为了保证代码的**灵活性**和**可扩展性**,我们可能不得不增加代码的复杂度。但除此之外,在这种问题固有复杂性的基础之上增加自制的复杂性,是十分不明智的做法——程序并非程序员炫技的场所,而应该是一件简约的艺术品。 16 | 17 | 一言以概之:如无必要,勿增实体。 18 | 19 | ## Loose Coupling⭐ 20 | 21 | Loose Coupling,即松耦合原则。这一原则指出:模块与模块之间的耦合(即相互关联的程度)应该越小越好,或者说,它们应该尽可能少地感知到对方的存在。 22 | 23 | 举一个例子吧(本例选自 *Clean C++* 一书): 24 | 25 | 考虑你有一台电灯,和一个用于控制电灯的开关: 26 | 27 | ```cpp 28 | class Lamp 29 | { 30 | public: 31 | void on() 32 | { 33 | 34 | } 35 | 36 | void off() 37 | { 38 | 39 | } 40 | } 41 | 42 | class Switch 43 | { 44 | public: 45 | Switch(Lamp& lamp): lamp(lamp) {} 46 | 47 | void toggle() 48 | { 49 | if (state) 50 | { 51 | state = false; 52 | lamp.off(); 53 | } 54 | 55 | else 56 | { 57 | state = true; 58 | lamp.on(); 59 | } 60 | } 61 | } 62 | ``` 63 | 64 | 在这样的设计方法下,开关可以工作,但可能会带来一个问题:`Switch`类中包含了`Lamp`类的引用,`Switch`类与`Lamp`类之间存在着强耦合关系——`Switch`类可以感知到`Lamp`类的存在。 65 | 66 | 这种写法不仅不符合常理,而且不便于维护和扩展:试想,如果我们想要用开关控制电扇、充电器等其它电器该怎么办?难道我们需要分别设计`SwitchForLamp`、`SwitchForFan`、`SwitchForCharger`类吗? 67 | 68 | 如何解决这类耦合问题?一个方法是:将两个类之间相关联的部分抽象成一个接口(interface),第二个类此时不需要包含第一个类的实例或引用,而只需要对接口负责,从而降低耦合度,提高程序的可扩展性。 69 | 70 | 以上程序可以改写如下(在C++中,接口可以使用虚基类实现): 71 | ```cpp 72 | #include 73 | 74 | class Switchable 75 | { 76 | public: 77 | virtual void on() = 0; 78 | virtual void off() = 0; 79 | }; 80 | 81 | class Switch 82 | { 83 | public: 84 | Switch(Switchable& switchable) : Switchable(switchable) {} 85 | void toggle() 86 | { 87 | if (state) 88 | { 89 | state = false; 90 | switchable.off(); 91 | } 92 | else 93 | { 94 | state = true; 95 | switchable.on(); 96 | } 97 | } 98 | 99 | private: 100 | Switchable& switchable; 101 | bool state {false}; 102 | }; 103 | 104 | class Lamp: public Switchable 105 | { 106 | public: 107 | void on() override 108 | { 109 | std::cout << "Lamp is on!" << std::endl; 110 | } 111 | 112 | void off() override 113 | { 114 | std::cout << "Lamp is off!" << std::endl; 115 | } 116 | }; 117 | 118 | class Fan: public Switchable 119 | { 120 | public: 121 | void on() override 122 | { 123 | std::cout << "Fan is on!" << std::endl; 124 | } 125 | 126 | void off() override 127 | { 128 | std::cout << "Fan is off!" << std::endl; 129 | } 130 | }; 131 | 132 | int main() 133 | { 134 | Lamp lamp; 135 | Switch switch1(lamp); 136 | switch1.toggle(); 137 | switch1.toggle(); 138 | 139 | Fan fan; 140 | Switch switch2(fan); 141 | switch2.toggle(); 142 | switch2.toggle(); 143 | } 144 | 145 | ``` 146 | 147 | 在以上更改中,开关与其它电器耦合的部分被抽象为一个接口`Switchable`,开关只需要对这一接口进行操作,避免了开关与具体电器类的耦合。 148 | 149 | ## SOLID⭐ 150 | 151 | SOLID是以下五大面向对象设计原则的缩写: 152 | * 单一功能原则(**S**ingle Responsibility Principle,SRP) 153 | * 开闭原则(**O**pen Closed Principle,OCP) 154 | * 里氏替换原则(**L**iskov Substitution Principle,LSP) 155 | * 接口隔离原则(**I**nterface Segregation Principle,ISP) 156 | * 依赖反转原则(**D**ependency Inversion Principle,DIP)。 157 | 158 | ### 单一功能原则 159 | 160 | 单一功能原则指出,每个软件单元(类、函数等),应该只有一个单一的、定义明确的责任。 161 | 162 | 如何界定单一责任?一个比较普适的定义是,改变该软件单元只能有一个原因。如果有多个原因,那么该单元就应该拆分。 163 | 164 | ### 开闭原则 165 | 166 | 开闭原则指出,软件单元(类、函数等)应该对于扩展是开放的,但是对于修改是封闭的。 167 | 168 | 具体来讲,如果我们需要给一个软件添加新的功能,我们通常不建议修改源码,而更加建议通过**继承**的方式。 169 | 170 | ### 里氏替换原则⭐ 171 | 172 | 里氏原则指出,派生类(子类)对象可以在程序中代替其基类(超类)对象。 173 | 174 | 换句话说,一个软件实体如果使用的是一个父类,那么也一定适用于其子类——把一个软件里面的父类都替换为它的子类,程序的行为是不会发生变化的。 175 | 176 | 利用这一原则,我们可以判断类与类之间的继承关系是否合适。 177 | 178 | 举个例子,假设我们拥有一个矩形类: 179 | ```cpp 180 | class Rectangle 181 | { 182 | public: 183 | Rectangle(int width, int height) : width(width), height(height) {} 184 | 185 | void setWidth(int width) 186 | { 187 | this->width = width; 188 | } 189 | 190 | void setHeight(int height) 191 | { 192 | this->height = height; 193 | } 194 | 195 | void setEdges(int width, int height) 196 | { 197 | this->width = width; 198 | this->height = height; 199 | } 200 | 201 | private: 202 | int width; 203 | int height; 204 | }; 205 | 206 | ``` 207 | 208 | 我们想要再新建立一个正方形类。根据初中几何知识:正方形是一种特殊的矩形——因此一种直观的想法是:让正方形类去继承矩形类: 209 | 210 | ```c++ 211 | class Square: public Rectangle 212 | { 213 | // ... 214 | }; 215 | ``` 216 | 217 | 但如果站在里氏替换原则的角度来看,这一设计是不科学的!比如我们考虑以下操作: 218 | 219 | ```c++ 220 | Rectangle rectangle; 221 | rectangle.setHeight(20); 222 | rectangle.setEdges(10, 5); 223 | ``` 224 | 225 | 根据里氏替换原则,派生类对象(Square)一定可以替换基类对象(Rectangle),假如我们进行这一替换: 226 | ```c++ 227 | Square square; 228 | square.setHeight(20); 229 | square.setEdges(10, 5); 230 | ``` 231 | 232 | 这时就出现了问题: 233 | * 第一个操作会产生歧义:该操作是只改变正方形的宽(这样会违背正方形的定义),还是同时改变正方形的长和宽(这样违背函数的字面意思)。 234 | * 第二个操作则会直接违背正方形的定义。 235 | 236 | 可以看到,派生类对象在此处替换基类对象会产生很多问题,这一继承是不科学的! 237 | 238 | ### 接口隔离原则⭐ 239 | 240 | 接口隔离原则指出,程序员在设计接口时应当将臃肿庞大的接口拆分成更小的和更具体的接口,让接口中只包含客户感兴趣的方法——使用多个专门的接口比使用单一的总接口要好。 241 | 242 | 换句话讲,接口约束了类的行为,是一种减轻代码耦合程度的好方法。但如果一个接口太过宽泛,可能会带来一些不必要的麻烦。举例说明: 243 | 244 | 我们想要定义一个“鸟”接口: 245 | 246 | ```c++ 247 | class Bird 248 | { 249 | public: 250 | virtual void eat() = 0; 251 | virtual void breathe() = 0; 252 | virtual void fly() = 0; 253 | }; 254 | ``` 255 | 256 | 在此基础上实现一个鸽子类,现在一切看上去都正常: 257 | ```c++ 258 | class Pigeon: public Bird 259 | { 260 | public: 261 | virtual void eat() override 262 | { 263 | // ... 264 | } 265 | 266 | virtual void breathe() override 267 | { 268 | // ... 269 | } 270 | 271 | virtual void fly() override 272 | { 273 | // ... 274 | } 275 | }; 276 | ``` 277 | 278 | 我们再实现一个企鹅类: 279 | ```c++ 280 | class Penguin: public Bird 281 | { 282 | public: 283 | virtual void eat() override 284 | { 285 | // ... 286 | } 287 | 288 | virtual void breathe() override 289 | { 290 | // ... 291 | } 292 | 293 | virtual void fly() override 294 | { 295 | // ??? 296 | } 297 | }; 298 | ``` 299 | 问题发生了。我们在一开始设计“鸟”这一接口时,想当然地以为所有地鸟类都会飞,却忽略了企鹅不会飞这一特例。 300 | 301 | 为了避免这样的情况发生,我们需要小心地将接口拆分: 302 | 303 | ```c++ 304 | class Lifeform 305 | { 306 | public: 307 | virtual void eat() = 0; 308 | virtual void breathe() = 0; 309 | }; 310 | 311 | class Flyable 312 | { 313 | public: 314 | virtual void fly() = 0; 315 | }; 316 | 317 | class Pigeon: public Lifeform, public Flyable 318 | { 319 | public: 320 | void eat() override 321 | { 322 | // ... 323 | } 324 | 325 | void breathe() override 326 | { 327 | // ... 328 | } 329 | 330 | void fly() override 331 | { 332 | // ... 333 | } 334 | }; 335 | 336 | class Penguin: public Lifeform 337 | { 338 | public: 339 | void eat() override 340 | { 341 | // ... 342 | } 343 | 344 | void breathe() override 345 | { 346 | // ... 347 | } 348 | }; 349 | ``` 350 | 如上文所示,所有的鸟类都需要呼吸和进食,我们可以大胆地将其封装为`Lifeform`接口,而并非所有鸟类都会飞,所以需要将其单独提取出来作为`Flyable`接口。在实现不同的鸟类时,我们将这些接口进行筛选组合即可。 351 | 352 | ### 依赖倒转原则⭐ 353 | 354 | 依赖倒转原则指出,在实际的开发场景中,类与类之间的依赖关系是十分复杂,在设计依赖关系时,高层模块不应该依赖低层模块,二者都应该依赖其抽象。 355 | 356 | 什么意思呢?考虑以下实例,一个用户在某在线网络平台上拥有一个账户,而这个账户又存储着该用户的信息。由此,两者不可避免地产生了下列的循环依赖关系——你中有我,我中有你: 357 | 358 | ```c++ 359 | 360 | class Account; 361 | 362 | class Customer 363 | { 364 | public: 365 | // ... 366 | void setAccount(Account *account) 367 | { 368 | customerAccount = account; 369 | } 370 | // ... 371 | private: 372 | Account *customerAccount; 373 | }; 374 | 375 | class Account 376 | { 377 | public: 378 | void setOwner(Customer *customer) 379 | { 380 | owner = customer; 381 | } 382 | 383 | private: 384 | Customer *owner; 385 | }; 386 | 387 | int main() 388 | { 389 | Account* account = new Account { }; 390 | Customer* customer = new Customer { }; 391 | account->setOwner(customer); 392 | customer->setAccount(account); 393 | } 394 | ``` 395 | 396 | 这会导致很严重的问题:首先代码的可读性由于循环依赖下降,而且两者的生命周期不相互独立——如果`Account`对象的生命周期先于`Customer`对象结束,`Customer`对象中将会产生一个空指针,调用`Customer`对象中的成员函数可能会导致程序崩溃。 397 | 398 | 而依赖倒转原则为解决此类问题提供了一套流程: 399 | 400 | 1. 不允许两个类中的其中一个直接访问另一个类,要想进行这种访问操作,需要通过接口。 401 | 2. 实现这个接口。 402 | 403 | 在本例中,我们不再使得`Account`类中包含有`Customer`类的指针,所有`Account`类需要访问`Customer`类的行为,都被定义进一个叫做`Owner`的接口中,而后,`Customer`类需要实现这个接口: 404 | 405 | ```c++ 406 | #include 407 | #include 408 | 409 | class Owner 410 | { 411 | public: 412 | virtual std::string getName() = 0; 413 | }; 414 | 415 | class Account; 416 | 417 | class Customer : public Owner 418 | { 419 | public: 420 | void setAccount(Account* account) 421 | { 422 | customerAccount = account; 423 | } 424 | virtual std::string getName() override 425 | { 426 | // return the Customer's name here... 427 | } 428 | // ... 429 | private: 430 | Account* customerAccount; 431 | // ... 432 | }; 433 | 434 | class Account 435 | { 436 | public: 437 | void setOwner(Owner* owner) 438 | { 439 | this->owner = owner; 440 | } 441 | //... 442 | private: 443 | Owner* owner; 444 | }; 445 | ``` 446 | 447 | 经过修改之后,`Account`类将不依赖于`Customer`类。 448 | 449 | ## 设计模式⭐ 450 | 451 | C++、C#、Python等语言为实现继承、多态等面向对象特性提供了丰富的语法。那么在具体的软件工程中,又该如何使用这些特性呢?这就是设计模式。设计模式是上述SOLID原则在软件工程中的具体体现。 452 | 453 | 设计模式共计分为3大类22小类: 454 | 455 | * **创建型模式**提供创建对象的机制, 增加已有代码的灵活性和可复用性。 456 | 457 | * **结构型模式**介绍如何将对象和类组装成较大的结构, 并同时保持结构的灵活和高效。 458 | 459 | * **行为模式**负责对象间的高效沟通和职责委派。 460 | 461 | 不同的设计模式之间有着相似的理念和重叠之处。合理利用设计模式可以让代码更加规范、更容易维护,但盲目使用设计模式也不是明智之举。 462 | 463 | 本讲将介绍一个难度较大,而且应用较为广泛的设计模式——**桥接模式**(属于结构型模式)。 464 | 465 | 桥接模式的定义如下:桥接模式是将类**抽象部分**与**实现部分**分离,使它们都可以独立地变化。 466 | 467 | ![桥接模式示意图](https://s2.loli.net/2022/07/03/TKqRdsVU73LSc5Q.png) 468 | 469 | 什么是**抽象部分**?什么是**实现部分**?让我们先考虑以下场景:一家奶茶店售卖不同种类的奶茶,奶茶既有不同的容量,也有不同的口味。如果我们只需要改变奶茶的容量,可以做出如下设计: 470 | 471 | ```cpp 472 | class IMilkTea // 通用接口 473 | { 474 | virtual void order() = 0; 475 | }; 476 | 477 | class MilkTeaSmallCup: public IMilkTea 478 | { 479 | void order() override 480 | { 481 | std::cout << "order info:" << std::endl; 482 | std::cout << "size: small cup" << std::endl; 483 | } 484 | }; 485 | 486 | class MilkTeaMediumCup: public IMilkTea 487 | { 488 | void order() override 489 | { 490 | std::cout << "order info:" << std::endl; 491 | std::cout << "size: medium cup" << std::endl; 492 | } 493 | }; 494 | 495 | class MilkTeaLargeCup: public IMilkTea 496 | { 497 | void order() override 498 | { 499 | std::cout << "order info:" << std::endl; 500 | std::cout << "size: large cup" << std::endl; 501 | } 502 | }; 503 | ``` 504 | 505 | 当类的变化只有一个维度时,继承的思路是比较直接而简单的。但当我们将“口味”也加入继承体系中,也就是当类的变化有两个维度时,沿用上面的思路将会使得类的数量急剧增长: 506 | 507 | ```c++ 508 | class MilkTeaSmallCupFairyGrass: public IMilkTea 509 | { 510 | void order() override 511 | { 512 | std::cout << "order info:" << std::endl; 513 | std::cout << "size: small cup" << std::endl; 514 | std::cout << "flavor: fairy grass" << std::endl; 515 | } 516 | }; 517 | 518 | class MilkTeaSmallCupPearl: public IMilkTea 519 | { 520 | void order() override 521 | { 522 | std::cout << "order info:" << std::endl; 523 | std::cout << "size: small cup" << std::endl; 524 | std::cout << "flavor: pearl" << std::endl; 525 | } 526 | }; 527 | 528 | // class MilkTeaMediumCupPearl, class MilkTeaLargeCupFairyGrass, ... 529 | ``` 530 | 531 | 问题的根源在于,我们试图在两个独立的维度(“容量”和“口味”)上扩展奶茶类。这时候,桥接模式就派上了用场:我们将容量视为**抽象部分**,将口味视为**实现部分**,并将两者桥接。 532 | 533 | > “抽象部分”和“实现部分”所承担的角色: 534 | > * 抽象部分:抽象化给出的定义,只提供高层控制逻辑,依赖于完成底层实际工作的实现对象。抽象部分保存一个对实现化对象的引用(指针)。 535 | > * 实现部分:给出实现化角色的通用接口,抽象部分仅能通过在这里声明的方法与实现对象交互。 536 | 537 | 例如在本例中,可以做如下修改: 538 | ```c++ 539 | // 实现化部分 540 | class IMilkTeaFlavorBase 541 | { 542 | public: 543 | virtual void GetFlavor() = 0; 544 | }; 545 | 546 | class MilkTeaPearl: public IMilkTeaFlavorBase 547 | { 548 | public: 549 | void GetFlavor() override 550 | { 551 | std::cout << "flavor: pearl" << std::endl; 552 | } 553 | }; 554 | 555 | class MilkTeaFairyGrass: public IMilkTeaFlavorBase 556 | { 557 | public: 558 | void GetFlavor() override 559 | { 560 | std::cout << "flavor: fairy grass" << std::endl; 561 | } 562 | }; 563 | 564 | // 抽象化部分 565 | class IMilkTeaSizeBase 566 | { 567 | public: 568 | virtual void SetFlavor(std::shared_ptr flavorBase) 569 | { 570 | this->flavorBase = flavorBase; 571 | } 572 | virtual void Order() = 0; 573 | protected: 574 | std::shared_ptr flavorBase; 575 | }; 576 | 577 | class MilkTeaSmall: public IMilkTeaSizeBase 578 | { 579 | public: 580 | void Order() override 581 | { 582 | std::cout << "size: small" << std::endl; 583 | flavorBase->GetFlavor(); 584 | } 585 | }; 586 | 587 | class MilkTeaMedium: public IMilkTeaSizeBase 588 | { 589 | public: 590 | void Order() override 591 | { 592 | std::cout << "size: medium" << std::endl; 593 | flavorBase->GetFlavor(); 594 | } 595 | }; 596 | 597 | class MilkTeaLarge: public IMilkTeaSizeBase 598 | { 599 | public: 600 | void Order() override 601 | { 602 | std::cout << "size: large" << std::endl; 603 | flavorBase->GetFlavor(); 604 | } 605 | }; 606 | 607 | // 使用方法 608 | int main() 609 | { 610 | // 大杯烧仙草 611 | std::shared_ptr milkTeaFairyGrass = std::make_shared(); 612 | std::shared_ptr milkTeaLargeWithFairyGrass = std::make_shared(); 613 | milkTeaLargeWithFairyGrass->SetFlavor(milkTeaFairyGrass); 614 | milkTeaLargeWithFairyGrass->Order(); 615 | } 616 | 617 | 618 | ``` 619 | 620 | 可以在上述示例中看到:抽象部分各类中,都含有一个实现部分的指针。如果需要访问实现部分的方法,可以通过该指针进行访问。这样,我们就通过桥接的方式分离了两个不同的维度,使得类的可扩展性更好。 621 | 622 | 由于篇幅所限,我们在此处不能对设计模式进行一一介绍,感兴趣的同学可以参考文末给出的阅读清单进行学习。 623 | 624 | ## 参考文献和荐读清单 625 | 626 | [Refactoring.Guru](https://refactoringguru.cn/) 该网站详细介绍了各设计模式的特点,并提供了不同编程语言的实例。 627 | 628 | [Clean C++](https://link.springer.com/content/pdf/10.1007%2F978-1-4842-2793-0.pdf) 这本书的侧重点不在介绍C++语法,而侧重于使用C++语言介绍如何写出可读性强、符合面向对象规范的程序,强推! 629 | 630 | -------------------------------------------------------------------------------- /Modern_Cpp/README.md: -------------------------------------------------------------------------------- 1 | # Modern C++ 选讲 2 | 3 | 鸟x 4 | 5 | [TOC] 6 | 7 | # 常量 8 | 9 | ## nullptr 10 | 11 | 替代NULL。C++11 引入了 `nullptr` 关键字,专门用来区分空指针、`0`。而 `nullptr` 的类型为 `nullptr_t`,能够隐式的转换为任何指针或成员指针的类型。 12 | 13 | ```C++ 14 | foo(0); // 调用 foo(int) 15 | // foo(NULL); // 该行不能通过编译 16 | foo(nullptr); // 调用 foo(char*) 17 | ``` 18 | 19 | ## constexpr 20 | 21 | 显式声明常量表达式。const修饰的变量**只在一定情况下**是常量表达式,这有时可能带来困扰。C++11 提供了 `constexpr` 让用户显式的声明函数或对象构造函数**在编译期**会成为常量表达式。 从 C++14 开始,`constexpr` 函数可以在内部使用局部变量、循环和分支等简单语句。 22 | 23 | ```C++ 24 | const int len_1 = 5; // 常量表达式 25 | const int len_2 = len_1 + 1; // 常量表达式 26 | constexpr int len_2_constexpr = 1 + 2 + 3; // 显式声明的常量表达式 27 | 28 | int len = 5; 29 | const int len_3 = len + 1; // 非常量表达式 30 | 31 | // 使用了static_assert,当其第一个参数为常量表达式时才不会报错 32 | static_assert(len_1, ""); 33 | static_assert(len_2, ""); 34 | static_assert(len_2_constexpr, ""); 35 | static_assert(len_3, ""); // 报错,说明len_3不是常量表达式 36 | ``` 37 | 38 | # 变量及其初始化 39 | 40 | ## if/switch 变量声明强化 41 | 42 | C++17 使得我们可以在 `if`(或 `switch`)中声明一个临时的变量: 43 | 44 | ```c++ 45 | // 将临时变量放到 if 语句内 46 | if (const std::vector::iterator itr = std::find(vec.begin(), vec.end(), 3); 47 | itr != vec.end()) { 48 | *itr = 4; 49 | } 50 | ``` 51 | 52 | ## 列表初始化⭐ 53 | 54 | C++11 引入了列表初始化,提供了统一的初始化方式。从此,我们可以使用brace-init-list(花括号)方便地进行初始化。 55 | 56 | ```C++ 57 | #include 58 | 59 | class Foo { 60 | public: 61 | Foo(int, int){} 62 | }; 63 | 64 | int main(){ 65 | int a[] {1, 2, 3}; // 数组 66 | int b{1}; // 变量 67 | std::vector v {1, 2}; // stl容器 68 | Foo foo {1, 2}; // 对象 69 | return 0; 70 | } 71 | ``` 72 | 73 | 此外,C++11引入了`std::initializer_list`,以支持对类的对象进行列表初始化。其允许构造函数或其他函数像参数一样使用初始化列表,还能将其作为普通函数的形参。在某些情况下,brace-init-list可以被自动推导为std::initializer_list。 74 | 75 | ```c++ 76 | #include 77 | #include 78 | 79 | class Foo { 80 | public: 81 | Foo(std::initializer_list list) { 82 | x = 0; 83 | for (std::initializer_list::iterator it = list.begin(); 84 | it != list.end(); ++it) x+=*it; 85 | std::cout< list) { 88 | for (std::initializer_list::iterator it = list.begin(); 89 | it != list.end(); ++it) std::cout<<*it< v; 97 | v.emplace_back(std::initializer_list{1, 2, 3}); 98 | //v.emplace_back({3, 2, 1}); // 报错,braced-init-list 未能推导为 std::initializer_list 99 | v[0].foo({1,2,3}); // braced-init-list 推导为 std::initializer_list 100 | return 0; 101 | } 102 | ``` 103 | 104 | ## 结构化绑定 105 | 106 | C++17给出的结构化绑定提供一种简单的方法直接从元组中拿到并定义元组中的元素。此外,还可以绑定数组、结构体。 107 | 108 | ```C++ 109 | #include 110 | #include 111 | #include 112 | 113 | struct A{ 114 | int i = 0; 115 | double j = 4.0; 116 | }; 117 | 118 | int main(){ 119 | auto [x, y, z] = std::make_tuple(1, 2.3, "456"); // 绑定元组 120 | 121 | std::array arr = {1, 2, 3}; 122 | auto [a, b, c] = arr; // 绑定数组 123 | 124 | A s; 125 | auto [m, n] = s; // 绑定结构体 126 | return 0; 127 | } 128 | ``` 129 | 130 | # 类型推导⭐ 131 | 132 | ## auto 133 | 134 | 从 C++11 起, 使用 auto 关键字进行类型推导。 135 | 136 | ```C++ 137 | class MagicFoo { 138 | public: 139 | std::vector vec; 140 | MagicFoo(std::initializer_list list) { 141 | // 不用再写冗长的迭代器类型名了 142 | for (auto it = list.begin(); it != list.end(); ++it) { 143 | vec.push_back(*it); 144 | } 145 | } 146 | }; 147 | 148 | auto i = 5; // i 被推导为 int 149 | auto arr = new auto(10); // arr 被推导为 int * 150 | ``` 151 | 152 | 从 C++ 20 起,`auto` 甚至能用于函数传参。 153 | 154 | ```C++ 155 | int add(auto x, auto y) { 156 | return x+y; 157 | } 158 | 159 | auto i = 5; // 被推导为 int 160 | auto j = 6; // 被推导为 int 161 | ``` 162 | 163 | **注意**:`auto` 还不能用于推导数组类型: 164 | 165 | ```c++ 166 | auto auto_arr2[10] = {arr}; // 错误, 无法推导数组元素类型 167 | ``` 168 | 169 | ## decltype 170 | 171 | `decltype` 关键字是为了解决 `auto` 关键字只能对变量进行类型推导的缺陷而出现的,可推导表达式的类型。 172 | 173 | ```C++ 174 | auto x = 1; 175 | auto y = 2; 176 | decltype(x+y) z; // z的类型是int 177 | ``` 178 | 179 | `std::is_same` 用于判断 `T` 和 `U` 这两个类型是否相等。 180 | 181 | ```C++ 182 | if (std::is_same::value) // 为真 183 | std::cout << "type x == int" << std::endl; 184 | if (std::is_same::value) // 为假 185 | std::cout << "type x == float" << std::endl; 186 | ``` 187 | 188 | ## 尾返回类型推导 189 | 190 | C++11 引入了一个尾返回类型(trailing return type),利用 `auto` 关键字将返回类型后置。 C++14 开始可以直接让普通函数具备返回值推导。 191 | 192 | ```c++ 193 | // after c++11 194 | template 195 | auto add2(T x, U y) -> decltype(x+y){ 196 | return x + y; 197 | } 198 | // after c++14 199 | template 200 | auto add3(T x, U y){ 201 | return x + y; 202 | } 203 | ``` 204 | 205 | ## decltype(auto) 206 | 207 | C++14引入的`decltype(auto)` 主要用于对转发函数或封装的返回类型进行推导,它使我们无需显式指定`decltype` 的参数表达式。 208 | 209 | ```C++ 210 | std::string lookup1(); 211 | std::string& lookup2(); 212 | 213 | // C++11 214 | std::string look_up_a_string_1() { 215 | return lookup1(); 216 | } 217 | std::string& look_up_a_string_2() { 218 | return lookup2(); 219 | } 220 | // after C++14 221 | decltype(auto) look_up_a_string_1() { 222 | return lookup1(); 223 | } 224 | decltype(auto) look_up_a_string_2() { 225 | return lookup2(); 226 | } 227 | ``` 228 | 229 | # 控制流 230 | 231 | ## 基于范围的for循环⭐ 232 | 233 | C++11 引入了基于范围的循环写法。 234 | 235 | ```C++ 236 | std::vector vec = {1, 2, 3, 4}; 237 | for (auto element : vec) 238 | std::cout << element << std::endl; // 不会改变vec的元素,用于读 239 | for (auto &element : vec) 240 | element += 1; // 可以改变vec的元素,用于写 241 | ``` 242 | 243 | # 面向对象 244 | 245 | ## 委托构造 246 | 247 | C++11 引入了委托构造的概念,这使得构造函数可以在同一个类中一个构造函数调用另一个构造函数。 248 | 249 | ```C++ 250 | #include 251 | class Base { 252 | public: 253 | int value1; 254 | int value2; 255 | Base() { 256 | value1 = 1; 257 | } 258 | Base(int value) : Base() { // 委托 Base() 构造函数 259 | value2 = value; 260 | } 261 | }; 262 | 263 | Base b(2); 264 | ``` 265 | 266 | ## 继承构造 267 | 268 | C++11 利用关键字 `using` 引入了继承构造函数。 269 | 270 | ```C++ 271 | #include 272 | class Base { 273 | public: 274 | int value1; 275 | int value2; 276 | Base() { 277 | value1 = 1; 278 | } 279 | Base(int value) : Base() { // 委托 Base() 构造函数 280 | value2 = value; 281 | } 282 | }; 283 | class Subclass : public Base { 284 | public: 285 | using Base::Base; // 继承构造 286 | }; 287 | ``` 288 | 289 | ## 显式虚函数重载 290 | 291 | C++11 引入了 `override` 和 `final` 这两个关键字来防止**意外重载虚函数**和基类的虚函数被删除后**子类的对应函数变为普通方法**的情况发生。 292 | 293 | ### override 294 | 295 | 当重载虚函数时,引入 `override` 关键字将显式的告知编译器进行重载,编译器将检查基函数是否存在这样的虚函数,否则将无法通过编译: 296 | 297 | ```C++ 298 | struct Base { 299 | virtual void foo(int); 300 | }; 301 | struct SubClass: Base { 302 | virtual void foo(int) override; // 合法 303 | virtual void foo(float) override; // 非法, 父类没有此虚函数 304 | }; 305 | ``` 306 | 307 | ### final 308 | 309 | `final` 则是为了防止类被继续继承以及终止虚函数继续重载引入的。 310 | 311 | ```C++ 312 | struct Base { 313 | virtual void foo() final; 314 | }; 315 | struct SubClass1 final: Base { 316 | }; // 合法 317 | 318 | struct SubClass2 : SubClass1 { 319 | }; // 非法, SubClass1 已 final 320 | 321 | struct SubClass3: Base { 322 | void foo(); // 非法, foo 已 final 323 | }; 324 | ``` 325 | 326 | ## 显式禁用默认函数 327 | 328 | C++11允许显式的声明采用或拒绝编译器默认生成的函数。 329 | 330 | ```C++ 331 | class Magic { 332 | public: 333 | Magic() = default; // 显式声明使用编译器生成的构造 334 | Magic& operator=(const Magic&) = delete; // 显式声明拒绝编译器生成默认赋值函数 335 | Magic(int magic_number); 336 | } 337 | ``` 338 | 339 | ## 强类型枚举 340 | 341 | C++11 引入了枚举类(enumeration class),并使用 `enum class` 的语法进行声明。枚举类实现了类型安全,首先他不能够被隐式的转换为整数,同时也不能够将其与整数数字进行比较, 更不可能对不同的枚举类型的枚举值进行比较。希望获得枚举值的值时,必须**显式**的进行类型转换。 342 | 343 | ```C++ 344 | #include 345 | enum class new_enum : unsigned int { 346 | value1, 347 | value2, 348 | value3 = 100, 349 | value4 = 100 350 | }; 351 | int main(){ 352 | if (new_enum::value3 == new_enum::value4) { 353 | std::cout << "new_enum::value3 == new_enum::value4" << std::endl; 354 | } 355 | std::cout<<(int)new_enum::value4; 356 | return 0; 357 | } 358 | ``` 359 | 360 | # Lambda 表达式⭐ 361 | 362 | Lambda 表达式是现代 C++ 中最重要的特性之一,而 Lambda 表达式,实际上就是提供了一个类似匿名函数的特性, 而匿名函数则是在需要一个函数,但是又不想费力去命名一个函数的情况下去使用的。 363 | 364 | ## 基础 365 | 366 | Lambda 表达式的基本语法如下: 367 | 368 | ```C++ 369 | [捕获列表](参数列表) mutable(可选) 异常属性 -> 返回类型 { 370 | // 函数体 371 | } 372 | ``` 373 | 374 | 所谓捕获列表,其实可以理解为参数的一种类型,Lambda 表达式内部函数体在默认情况下是不能够使用函数体外部的变量的, 这时候捕获列表可以起到**传递外部数据**的作用。根据传递的行为,捕获列表也分为以下几种。 375 | 376 | ### 值捕获 377 | 378 | 与参数传值类似,值捕获的前提是变量可以拷贝,不同之处则在于,被捕获的变量在 Lambda 表达式被创建时拷贝, 而非调用时才拷贝。 379 | 380 | ```C++ 381 | int value = 1; 382 | auto copy_value = [value] { 383 | return value; 384 | }; 385 | value = 100; 386 | auto stored_value = copy_value(); 387 | // stored_value == 1, 而 value == 100. 388 | // 因为 copy_value 在创建时就保存了一份 value 的拷贝 389 | ``` 390 | 391 | ### 引用捕获 392 | 393 | 与引用传参类似,引用捕获保存的是引用,值会发生变化。 394 | 395 | ```C++ 396 | int value = 1; 397 | auto copy_value = [&value] { 398 | return value; 399 | }; 400 | value = 100; 401 | auto stored_value = copy_value(); 402 | // 这时, stored_value == 100, value == 100. 403 | // 因为 copy_value 保存的是引用 404 | ``` 405 | 406 | ### 隐式捕获 407 | 408 | 可以在捕获列表中写一个 `&` 或 `=` 向编译器声明采用引用捕获或者值捕获。 409 | 410 | ```C++ 411 | int value = 1; 412 | auto copy_value = [&] { 413 | return value; 414 | }; 415 | ``` 416 | 417 | 捕获列表常用的四种形式: 418 | 419 | - [] 空捕获列表 420 | - [name1, name2, ...] 捕获一系列变量 421 | - [&] 引用捕获, 让编译器自行推导引用列表 422 | - [=] 值捕获, 让编译器自行推导值捕获列表 423 | 424 | ### 表达式捕获 425 | 426 | 上面提到的值捕获、引用捕获都是已经在外层作用域声明的变量,因此这些捕获方式捕获的均为左值,而不能捕获右值。C++14允许捕获的成员用任意的表达式进行初始化,这就允许了右值的捕获, 被声明的捕获变量类型会根据表达式进行判断,判断方式与使用 `auto` 本质上是相同的。 427 | 428 | ```C++ 429 | #include // std::make_unique 430 | #include // std::move, 将important转换为右值 431 | 432 | void lambda_expression_capture() { 433 | auto important = std::make_unique(1); 434 | auto add = [v1 = 1, v2 = std::move(important)](int x, int y) -> int { 435 | return x+y+v1+(*v2); 436 | }; 437 | std::cout << add(3,4) << std::endl; 438 | } 439 | ``` 440 | 441 | ## 泛型Lambda 442 | 443 | 从 C++14 开始, Lambda 函数的形式参数可以使用 `auto` 关键字来自动推导参数类型。 444 | 445 | ```C++ 446 | auto add = [](auto x, auto y) { 447 | return x+y; 448 | }; 449 | 450 | add(1, 2); 451 | add(1.1, 2.2); 452 | ``` 453 | 454 | # 函数对象包装器 455 | 456 | 这部分内容虽然属于标准库的一部分,但是从本质上来看,它却增强了 C++ 语言运行时的能力。 457 | 458 | ## `std::function`⭐ 459 | 460 | Lambda 表达式的本质是一个和函数对象类型相似的类类型(称为闭包类型)的对象(称为闭包对象)。当 Lambda 表达式的捕获列表为空时,闭包对象还能够转换为函数指针值进行传递: 461 | 462 | ```C++ 463 | using foo = void(int); // 定义函数类型 464 | void functional(foo f) { // 定义在参数列表中的函数类型 foo 被视为退化后的函数指针类型 foo* 465 | f(1); // 通过函数指针调用函数 466 | } 467 | 468 | auto f = [](int value) { 469 | std::cout << value << std::endl; 470 | };// f是闭包对象 471 | functional(f); // 传递闭包对象,隐式转换为 foo* 类型的函数指针值 472 | f(1); // lambda 表达式调用 473 | ``` 474 | 475 | 上面的代码给出了两种不同的调用形式,一种是将 Lambda 作为函数类型传递进行调用, 而另一种则是直接调用 Lambda 表达式,在 C++11 中,统一了这些概念,将能够被调用的对象的类型, 统一称之为可调用类型。 476 | 477 | C++11 `std::function` 是一种通用、多态的函数封装, 它的实例可以对任何可以调用的目标实体进行存储、复制和调用操作,可以理解为**函数的容器**。 478 | 479 | ```C++ 480 | #include 481 | 482 | int foo(int para) { 483 | return para; 484 | } 485 | 486 | // std::function 包装了一个返回值为 int, 参数为 int 的函数 487 | std::function func = foo; 488 | 489 | int important = 10; 490 | std::function func2 = [&](int value) -> int { 491 | return 1+value+important; 492 | }; 493 | func(10); 494 | func2(10); 495 | ``` 496 | 497 | ## `std::bind`和`std::placeholder` 498 | 499 | 我们有时候可能并不一定能够一次性获得调用某个函数的全部参数,通过 `std::bind` 可以将部分调用参数提前绑定到函数身上成为一个新的对象,然后在参数齐全后,完成调用。 500 | 501 | ```C++ 502 | int foo(int a, int b, int c) { 503 | return a+b+c; 504 | } 505 | 506 | // 将参数1,2绑定到函数 foo 上,但是使用 std::placeholders::_1 来对第一个参数进行占位 507 | auto bindFoo = std::bind(foo, std::placeholders::_1, 1, 2); 508 | // 这时调用 bindFoo 时,只需要提供第一个参数即可 509 | bindFoo(1); 510 | ``` 511 | 512 | C++14之后,lambda表达式可以完全替代`std::bind`,详见https://stackoverflow.com/questions/17363003/why-use-stdbind-over-lambdas-in-c14,此处不再多叙。 513 | 514 | # 右值引用⭐ 515 | 516 | 右值引用是 C++11 引入的与 Lambda 表达式齐名的重要特性之一。它的引入解决了 C++ 中大量的历史遗留问题, 消除了诸如 `std::vector`、`std::string` 之类的额外开销, 也才使得函数对象容器 `std::function` 成为了可能。 517 | 518 | ## 左值、右值的纯右值、亡值、右值 519 | 520 | **左值(lvalue, left value)**,顾名思义就是赋值符号左边的值。准确来说, 左值是表达式(不一定是赋值表达式)后依然存在的持久对象。(**注意**:字符串字面量为左值,如"hello") 521 | 522 | **右值(rvalue, right value)**,右边的值,是指表达式结束后就不再存在的临时对象(或引用)。 523 | 524 | 而 C++11 中为了引入强大的右值引用,将右值的概念进行了进一步的划分,分为:纯右值、将亡值。 525 | 526 | **纯右值(prvalue, pure rvalue)**,纯粹的右值,要么是纯粹的字面量,例如 `10`, `true`; 要么是临时对象,例如 `-1`,`1+2`,a++的结果。非引用返回的临时变量、运算表达式产生的临时变量、Lambda 表达式都属于纯右值。 527 | 528 | **亡值(xvalue, expiring value)**,是 C++11 为了引入右值引用而提出的概念(因此在传统 C++ 中,只有右值这一个概念),也就是即将被销毁、却能够被移动(move)的值。 529 | 530 | ## 右值引用和左值引用 531 | 532 | 顾名思义,右值引用可以引用一切右值(包括纯右值和亡值):`T &&`,其中`T`为非左值引用类型(若T为左值引用类型,则结果仍是左值引用)。 **右值引用的声明让这个临时对象的生命周期得以延长,只要右值引用生命期还未结束,那么这个临时对象的生命期也不会结束。** 533 | 534 | C++11 提供了 `std::move` 这个方法将左值参数无条件的转换为右值, 有了它我们就能够方便的获得一个右值引用(匿名右值引用)。 535 | 536 | ```C++ 537 | #include 538 | #include // std::move 539 | void reference(std::string& str) { 540 | std::cout << "左值" << std::endl; 541 | } 542 | void reference(std::string&& str) { 543 | std::cout << "右值" << std::endl; 544 | } 545 | int main(){ 546 | std::string lv0 = "string,"; // lv0 是一个左值 547 | 548 | std::string& lv1 = lv0; // lv1 是左值引用 549 | 550 | // std::string&& r1 = lv0; // 非法, 右值引用不能引用左值 551 | std::string&& rv1 = std::move(lv0); // 合法, std::move可以将左值转换为右值 552 | 553 | const std::string& lv2 = lv0 + lv0; // 合法, 常量左值引用能够延长临时变量的生命周期 554 | // lv2 += "Test"; // 非法, 常量引用无法被修改 555 | 556 | std::string&& rv2 = lv0 + lv2; // 合法, 右值引用延长临时对象生命周期 557 | rv2 += "Test"; // 合法, 非常量引用能够修改临时变量 558 | 559 | reference(lv1); // lv1是左值引用,为左值 560 | reference(rv2); // rv2是一个具名右值引用,为左值,见完美转发 561 | reference(std::move(lv0)); // 匿名右值引用,为右值 562 | return 0; 563 | } 564 | ``` 565 | 566 | ## 移动语义 567 | 568 | 右值引用的出现恰好解决了传统C++没有区分『移动』和『拷贝』的概念的问题。 569 | 570 | ```C++ 571 | #include 572 | class A { 573 | public: 574 | int *pointer; 575 | A():pointer(new int(1)) { 576 | std::cout << "构造" << pointer << std::endl; 577 | } 578 | A(A& a):pointer(new int(*a.pointer)) { 579 | std::cout << "拷贝" << pointer << std::endl; 580 | } // 无意义的对象拷贝 581 | A(A&& a):pointer(a.pointer) { 582 | a.pointer = nullptr; 583 | std::cout << "移动" << pointer << std::endl; 584 | } 585 | ~A(){ 586 | std::cout << "析构" << pointer << std::endl; 587 | delete pointer; 588 | } 589 | }; 590 | A return_rvalue(bool test) { 591 | A a,b; 592 | if(test) return a; // 等价于 static_cast(a); 593 | else return b; // 等价于 static_cast(b); 594 | } 595 | int main(){ 596 | A obj = return_rvalue(false); 597 | // 输出: 598 | // 构造0xa9eeb0 599 | // 构造0xa9fee0 600 | // 移动0xa9fee0 601 | // 析构0 602 | // 析构0xa9eeb0 603 | // 析构0xa9fee0 604 | return 0; 605 | } 606 | ``` 607 | 608 | 在上面的代码中: 609 | 610 | 1. 首先会在 `return_rvalue` 内部构造两个 `A` 对象,于是获得两个构造函数的输出; 611 | 2. 函数返回后,产生一个亡值,被 `A` 的移动构造(`A(A&&)`)引用,从而延长生命周期,并将这个右值中的指针拿到,保存到了 `obj` 中,而亡值的指针被设置为 `nullptr`。 612 | 613 | 下面是涉及标准库的例子,使用右值引用避免无意义拷贝以提升性能。 614 | 615 | ```C++ 616 | std::string str = "Hello world."; 617 | std::vector v; 618 | 619 | // 将使用 push_back(const T&), 即产生拷贝行为 620 | v.push_back(str); 621 | // 将输出 "str: Hello world." 622 | std::cout << "str: " << str << std::endl; 623 | 624 | // 将使用 push_back(const T&&), 不会出现拷贝行为 625 | // 而整个字符串会被移动到 vector 中,所以有时候 std::move 会用来减少拷贝出现的开销 626 | // 这步操作后, str 中的值会变为空 627 | v.push_back(std::move(str)); 628 | // 将输出 "str: " 629 | std::cout << "str: " << str << std::endl; 630 | ``` 631 | 632 | ## 完美转发 633 | 634 | 一个具名右值引用其实是一个左值。这就为我们进行参数转发(传递)造成了问题: 635 | 636 | ```C++ 637 | void reference(int& v) { 638 | std::cout << "左值" << std::endl; 639 | } 640 | void reference(int&& v) { 641 | std::cout << "右值" << std::endl; 642 | } 643 | template 644 | void pass(T&& v) { 645 | reference(v); // v是具名右值引用,为左值,故始终调用 reference(int&) 646 | } 647 | 648 | std::cout << "传递右值:" << std::endl; 649 | pass(1); // 1是右值, 但输出是左值 650 | 651 | std::cout << "传递左值:" << std::endl; 652 | int l = 1; 653 | pass(l); // l 是左值, 输出左值 654 | ``` 655 | 656 | 为了解决这个问题,我们应该使用 `std::forward` 来进行参数的转发(传递): 657 | 658 | ```C++ 659 | #include 660 | #include 661 | void reference(int& v) { 662 | std::cout << "左值引用" << std::endl; 663 | } 664 | void reference(int&& v) { 665 | std::cout << "右值引用" << std::endl; 666 | } 667 | template 668 | void pass(T&& v) {// v是左值 669 | reference(v);// 普通传参,永远输出左值引用 670 | reference(std::move(v));// std::move 传参,永远输出右值引用 671 | reference(std::forward(v));// std::forward 传参 672 | reference(static_cast(v));// static_cast 传参 673 | } 674 | int main(){ 675 | int a = 1; 676 | pass(1); 677 | pass(a); 678 | return 0; 679 | } 680 | ``` 681 | 682 | `std::forward` 不会造成任何多余的拷贝,同时**完美转发**函数的实参给内部调用的其他函数: 683 | 684 | 当实参是右值(int、或者int 的引用),T被推导为 int,T&& 是 int&&; 685 | 当实参是左值(int、或者int 的引用),T被推导为 int&,T&& 是 int&。 686 | 687 | `std::forward` 和 `std::move` 一样,只是类型转换,`std::move` 单纯的将左值转化为右值, `std::forward` 也只是单纯的将参数做了一个类型的转换,从现象上来看, `std::forward(v)` 和 `static_cast(v)` 是完全一样的。 688 | 689 | # 智能指针与内存管理 690 | 691 | ## RAII 与引用计数 692 | 693 | 引用计数这种计数是为了防止内存泄露而产生的。 基本想法是对于动态分配的对象,进行引用计数,每当增加一次对同一个对象的引用,那么引用对象的引用计数就会增加一次, 每删除一次引用,引用计数就会减一,当一个对象的引用计数减为零时,就自动删除指向的堆内存。 694 | 695 | 在传统 C++ 中,『记得』手动释放资源,总不是最佳实践。因为我们很有可能就忘记了去释放资源而导致泄露。 所以通常的做法是对于一个对象而言,我们在构造函数的时候申请空间,而在析构函数(在离开作用域时调用)的时候释放空间, 也就是我们常说的 RAII 资源获取即初始化技术。 696 | 697 | C++11 引入智能指针的概念,让程序员不再需要关心手动释放内存。使用它们需要包含头文件 ``。 698 | 699 | ## `std::unique_ptr` 700 | 701 | `std::unique_ptr` 是一种独占的智能指针,它禁止其他智能指针与其共享同一个对象,从而保证代码的安全。 702 | 703 | ```C++ 704 | std::unique_ptr pointer = std::make_unique(10); // make_unique 从 C++14 引入 705 | std::unique_ptr pointer2 = pointer; // 非法 706 | ``` 707 | 708 | >`make_unique` 并不复杂,C++11 没有提供 `std::make_unique`,可以自行实现: 709 | > 710 | >```C++ 711 | >template 712 | >std::unique_ptr make_unique( Args&& ...args ) { 713 | > return std::unique_ptr( new T( std::forward(args)... ) ); 714 | >} 715 | >``` 716 | 717 | 既然是独占,换句话说就是不可复制。但是,我们可以利用 `std::move` 将其转移给其他的 `unique_ptr`。 718 | 719 | ```C++ 720 | #include 721 | 722 | struct Foo { 723 | Foo() { std::cout << "Foo::Foo" << std::endl; } 724 | ~Foo() { std::cout << "Foo::~Foo" << std::endl; } 725 | void foo() { std::cout << "Foo::foo" << std::endl; } 726 | }; 727 | 728 | void f(const Foo &) { 729 | std::cout << "f(const Foo&)" << std::endl; 730 | } 731 | 732 | int main() { 733 | std::unique_ptr p1(std::make_unique()); 734 | // p1 不空, 输出 735 | if (p1) p1->foo(); 736 | { 737 | std::unique_ptr p2(std::move(p1)); 738 | // p2 不空, 输出 739 | f(*p2); 740 | // p2 不空, 输出 741 | if(p2) p2->foo(); 742 | // p1 为空, 无输出 743 | if(p1) p1->foo(); 744 | p1 = std::move(p2); 745 | // p2 为空, 无输出 746 | if(p2) p2->foo(); 747 | std::cout << "p2 被销毁" << std::endl; 748 | } 749 | // p1 不空, 输出 750 | if (p1) p1->foo(); 751 | // Foo 的实例会在离开作用域时被销毁 752 | } 753 | ``` 754 | 755 | 此外,由于独占,`std::unique_ptr`不会有引用计数的开销,因此常常是首选。 756 | 757 | ## `std::shared_ptr` 758 | 759 | `std::shared_ptr` 是一种智能指针,它能够记录多少个 `shared_ptr` 共同指向一个对象,从而消除显式的调用 `delete`,当引用计数变为零的时候就会将对象自动删除。 760 | 761 | 但还不够,因为使用 `std::shared_ptr` 仍然需要使用 `new` 来调用,这使得代码出现了某种程度上的不对称。 762 | 763 | `std::make_shared` 就能够用来消除显式的使用 `new`,会分配创建传入参数中的对象, 并返回这个对象类型的`std::shared_ptr`指针。 764 | 765 | ```C++ 766 | #include 767 | #include 768 | void foo(std::shared_ptr i) { 769 | (*i)++; 770 | } 771 | int main() { 772 | // Constructed a std::shared_ptr 773 | auto pointer = std::make_shared(10); 774 | foo(pointer); 775 | std::cout << *pointer << std::endl; // 11 776 | // The shared_ptr will be destructed before leaving the scope 777 | return 0; 778 | } 779 | ``` 780 | 781 | `std::shared_ptr` 可以通过 `get()` 方法来获取原始指针,通过 `reset()` 来减少一个引用计数, 并通过`use_count()`来查看一个对象的引用计数。 782 | 783 | ```C++ 784 | auto pointer = std::make_shared(10); 785 | auto pointer2 = pointer; // 引用计数+1 786 | auto pointer3 = pointer; // 引用计数+1 787 | int *p = pointer.get(); // 这样不会增加引用计数 788 | std::cout << "pointer.use_count() = " << pointer.use_count() << std::endl; // 3 789 | std::cout << "pointer2.use_count() = " << pointer2.use_count() << std::endl; // 3 790 | std::cout << "pointer3.use_count() = " << pointer3.use_count() << std::endl; // 3 791 | 792 | pointer2.reset(); 793 | std::cout << "reset pointer2:" << std::endl; 794 | std::cout << "pointer.use_count() = " << pointer.use_count() << std::endl; // 2 795 | std::cout << "pointer2.use_count() = " << pointer2.use_count() << std::endl; // 0, pointer2 已 reset 796 | std::cout << "pointer3.use_count() = " << pointer3.use_count() << std::endl; // 2 797 | pointer3.reset(); 798 | std::cout << "reset pointer3:" << std::endl; 799 | std::cout << "pointer.use_count() = " << pointer.use_count() << std::endl; // 1 800 | std::cout << "pointer2.use_count() = " << pointer2.use_count() << std::endl; // 0 801 | std::cout << "pointer3.use_count() = " << pointer3.use_count() << std::endl; // 0, pointer3 已 reset 802 | ``` 803 | 804 | ## `std::weak_ptr` 805 | 806 | `std::shared_ptr`引入了引用成环的问题。 807 | 808 | ```C++ 809 | struct A; 810 | struct B; 811 | 812 | struct A { 813 | std::shared_ptr pointer; 814 | ~A() { 815 | std::cout << "A 被销毁" << std::endl; 816 | } 817 | }; 818 | struct B { 819 | std::shared_ptr pointer; 820 | ~B() { 821 | std::cout << "B 被销毁" << std::endl; 822 | } 823 | }; 824 | int main() { 825 | auto a = std::make_shared(); 826 | auto b = std::make_shared(); 827 | a->pointer = b; 828 | b->pointer = a; 829 | // 结果是A和B依然不能被销毁,因为二者间形成了一个环,导致两个shared_ptr引用计数都为1 830 | } 831 | ``` 832 | 833 | 解决这个问题的办法就是使用弱引用指针 `std::weak_ptr`,`std::weak_ptr`是一种弱引用(相比较而言 `std::shared_ptr` 就是一种强引用)。弱引用不会引起引用计数增加。 834 | 835 | 对于上面的代码,将A或B中的任意一个`std::shared_ptr` 改为 `std::weak_ptr`即可解决问题。 836 | 837 | ```C++ 838 | struct A { 839 | std::shared_ptr pointer; 840 | ~A() { 841 | std::cout << "A 被销毁" << std::endl; 842 | } 843 | }; 844 | struct B { 845 | std::weak_ptr pointer; 846 | ~B() { 847 | std::cout << "B 被销毁" << std::endl; 848 | } 849 | }; 850 | // 将B的智能指针改为weak_ptr,这样程序结束时A的引用计数就会变成0而销毁,B的引用计数随之变为0而销毁 851 | ``` 852 | 853 | `std::weak_ptr` 没有 `*` 运算符和 `->` 运算符,所以不能够对资源进行操作,它可以用于检查 `std::shared_ptr` 是否存在,其 `expired()` 方法能在资源未被释放时,会返回 `false`,否则返回 `true`;除此之外,它也可以用于获取指向原始对象的 `std::shared_ptr` 指针,其 `lock()` 方法在原始对象未被释放时,返回一个指向原始对象的 `std::shared_ptr` 指针,进而访问原始对象的资源,否则返回默认构造的`std::shared_ptr`(即未托管任何指针)。 854 | 855 | ```C++ 856 | std::weak_ptr b; 857 | { 858 | auto a = std::make_shared(1); 859 | b = a; 860 | if(auto c = b.lock()) { // 输出 861 | std::cout << *c << std::endl; 862 | } 863 | } 864 | if(auto c = b.expired()) { // 输出 865 | std::cout << "b is expired" << std::endl; 866 | } 867 | ``` 868 | 869 | # 并行与并发 870 | 871 | ## 并发基础 872 | 873 | `std::thread` 用于创建一个执行的线程实例,所以它是一切并发编程的基础,使用时需要包含 `` 头文件, 它提供了很多基本的线程操作,例如 `get_id()` 来获取所创建线程的线程 ID,使用 `join()` 来加入一个线程等等。 874 | 875 | ```C++ 876 | #include 877 | 878 | int main() { 879 | std::thread t([](){ 880 | std::cout << "hello world." << std::endl; 881 | }); 882 | t.join(); 883 | return 0; 884 | } 885 | ``` 886 | 887 | ## 互斥量与临界区 888 | 889 | 互斥量`mutex` 是并发技术中的核心之一。 C++11 引入了 `mutex` 相关的类,其所有相关的函数都放在 `` 头文件中。 890 | 891 | `std::mutex` 是 C++11 中最基本的 `mutex` 类,通过实例化 `std::mutex` 可以创建互斥量, 而通过其成员函数 `lock()` 可以进行上锁,`unlock()` 可以进行解锁。 但是在实际编写代码的过程中,最好不去直接调用成员函数, 因为调用成员函数就需要在每个临界区的出口处调用 `unlock()`,当然,还包括异常。 这时候 C++11 还为互斥量提供了一个 RAII 语法的模板类 `std::lock_guard`。 RAII 在不失代码简洁性的同时,很好的保证了代码的异常安全性。 892 | 893 | 在 RAII 用法下,对于临界区的互斥量的创建只需要在作用域的开始部分。 894 | 895 | ```C++ 896 | #include 897 | #include 898 | 899 | int v = 1; 900 | 901 | void critical_section(int change_v) { 902 | static std::mutex mtx; 903 | std::lock_guard lock(mtx); 904 | // 执行竞争操作 905 | v = change_v; 906 | // 离开此作用域后 mtx 会被释放 907 | } 908 | ``` 909 | 910 | 这样的代码也是异常安全的。 无论临界区正常返回、还是在中途抛出异常,都会自动调用 `unlock()`。 911 | 912 | 而 `std::unique_lock` 则相对于 `std::lock_guard` 出现的,`std::unique_lock` 更加灵活, `std::unique_lock` 的对象会以独占所有权(没有其他的 `unique_lock` 对象同时拥有某个 `mutex` 对象的所有权) 的方式管理 `mutex` 对象上的上锁和解锁的操作。所以在并发编程中,推荐使用 `std::unique_lock`。 913 | 914 | `std::lock_guard` 不能显式的调用 `lock` 和 `unlock`, 而 `std::unique_lock` 可以在声明后的任意位置调用, 可以缩小锁的作用范围,提供更高的并发度。 915 | 916 | 如果你用到了条件变量 `std::condition_variable::wait` 则必须使用 `std::unique_lock` 作为参数。 917 | 918 | ```C++ 919 | #include 920 | #include 921 | 922 | int v = 1; 923 | 924 | void critical_section(int change_v) { 925 | static std::mutex mtx; 926 | std::unique_lock lock(mtx); 927 | // 执行竞争操作 928 | v = change_v; 929 | // 将锁进行释放 930 | lock.unlock(); 931 | 932 | // 在此期间,任何人都可以抢夺 v 的持有权 933 | 934 | // 开始另一组竞争操作,再次加锁 935 | lock.lock(); 936 | v += 1; 937 | // 离开此作用域后 mtx 会被释放 938 | } 939 | ``` 940 | 941 | ## 期值future 942 | 943 | 如果我们的主线程 A 希望新开辟一个线程 B 去执行某个我们预期的任务,并返回一个结果。 而这时候,线程 A 可能正在忙其他的事情,无暇顾及 B 的结果, 所以我们会很自然的希望能够在某个特定的时间获得线程 B 的结果。 944 | 945 | 在 C++11 的 `std::future` 被引入之前,通常的做法是: 创建一个线程 A,在线程 A 里启动任务 B,当准备完毕后发送一个事件,并将结果保存在全局变量中。 而主函数线程 A 里正在做其他的事情,当需要结果的时候,调用一个线程等待函数来获得执行的结果。 946 | 947 | 而 C++11 提供的 `std::future` 简化了这个流程,可以用来获取异步任务的结果。 自然地,我们很容易能够想象到把它作为一种简单的线程同步手段,即屏障(barrier)。 948 | 949 | 标准库中的`std::async`可在其中调用的函数执行完成前就返回其`std::future`对象,对该对象使用get()即可阻塞程序到函数执行完成时取得返回值: 950 | 951 | ```C++ 952 | #include 953 | #include 954 | 955 | int main(){ 956 | auto result = std::async(std::launch::async, [](){return 7;}); 957 | std::cout << "waiting..." << std::endl; 958 | std::cout << "future result is " << result.get() << std::endl; 959 | return 0; 960 | } 961 | ``` 962 | 963 | ## 条件变量 964 | 965 | 条件变量 `std::condition_variable` 是为解决死锁而生。 比如,线程可能需要等待某个条件为真才能继续执行, 而一个忙等待循环中可能会导致所有其他线程都无法进入临界区使得条件为真时,就会发生死锁。 所以,`condition_variable` 实例被创建出现主要就是用于唤醒等待线程从而避免死锁。 `std::condition_variable`的 `notify_one()` 用于唤醒一个线程; `notify_all()` 则是通知所有线程。下面是一个生产者和消费者模型的例子。 966 | 967 | ```C++ 968 | #include 969 | #include 970 | #include 971 | #include 972 | #include 973 | #include 974 | 975 | 976 | int main() { 977 | std::queue produced_nums; 978 | std::mutex mtx; 979 | std::condition_variable cv; 980 | 981 | // 生产者 982 | auto producer = [&]() { 983 | for (int i = 0; ; i++) { 984 | std::this_thread::sleep_for(std::chrono::milliseconds(900)); // 生产时间 985 | { 986 | std::unique_lock lock(mtx); 987 | // 生产1个 988 | std::cout << "producing " << i << std::endl; 989 | produced_nums.push(i); 990 | 991 | cv.notify_one(); 992 | } 993 | } 994 | }; 995 | // 消费者 996 | auto consumer = [&]() { 997 | while (true) { 998 | std::this_thread::sleep_for(std::chrono::milliseconds(1000)); // 消费慢于生产 999 | { 1000 | std::unique_lock lock(mtx); 1001 | while (produced_nums.empty()) { // 避免虚假唤醒 1002 | cv.wait(lock); 1003 | } 1004 | // 等价写法 cv.wait(lock, [&]{ return !produced_nums.empty(); }); 1005 | // 消费1个 1006 | std::cout << "consuming " << produced_nums.front() << std::endl; 1007 | produced_nums.pop(); 1008 | } 1009 | } 1010 | }; 1011 | 1012 | // 分别在不同的线程中运行 1013 | std::thread p(producer); 1014 | std::thread cs[2]; 1015 | for (int i = 0; i < 2; ++i) { 1016 | cs[i] = std::thread(consumer); 1017 | } 1018 | p.join(); 1019 | for (int i = 0; i < 2; ++i) { 1020 | cs[i].join(); 1021 | } 1022 | return 0; 1023 | } 1024 | ``` 1025 | 1026 | # 杂项 1027 | 1028 | ## noexcept 1029 | 1030 | C++11 将异常的声明简化为以下两种情况: 1031 | 1032 | 1. 函数可能抛出任何异常 1033 | 2. 函数不能抛出任何异常 1034 | 1035 | 并使用 `noexcept` 对这两种行为进行限制,例如: 1036 | 1037 | ```C++ 1038 | void may_throw(); // 可能抛出异常 1039 | void no_throw() noexcept; // 不可能抛出异常 1040 | ``` 1041 | 1042 | 使用 `noexcept` 修饰过的函数如果抛出异常,编译器会使用 `std::terminate()` 来立即终止程序运行。 1043 | 1044 | `noexcept` 还能够做操作符,用于操作一个表达式,当表达式无异常时,返回 `true`,否则返回 `false`。 1045 | 1046 | ```C++ 1047 | #include 1048 | void may_throw() { 1049 | throw true; 1050 | } 1051 | auto non_block_throw = []{ 1052 | may_throw(); 1053 | }; 1054 | void no_throw() noexcept { 1055 | return; 1056 | } 1057 | 1058 | auto block_throw = []() noexcept { 1059 | no_throw(); 1060 | }; 1061 | int main() 1062 | { 1063 | std::cout << std::boolalpha 1064 | << "may_throw() noexcept? " << noexcept(may_throw()) << std::endl 1065 | << "no_throw() noexcept? " << noexcept(no_throw()) << std::endl 1066 | << "lmay_throw() noexcept? " << noexcept(non_block_throw()) << std::endl 1067 | << "lno_throw() noexcept? " << noexcept(block_throw()) << std::endl; 1068 | return 0; 1069 | } 1070 | ``` 1071 | 1072 | ```C++ 1073 | try { 1074 | may_throw(); 1075 | } catch (...) { 1076 | std::cout << "捕获异常, 来自 may_throw()" << std::endl; 1077 | } 1078 | try { 1079 | non_block_throw(); 1080 | } catch (...) { 1081 | std::cout << "捕获异常, 来自 non_block_throw()" << std::endl; 1082 | } 1083 | try { 1084 | block_throw(); 1085 | } catch (...) { 1086 | std::cout << "捕获异常, 来自 block_throw()" << std::endl; 1087 | } 1088 | // output 1089 | // 捕获异常, 来自 may_throw() 1090 | // 捕获异常, 来自 non_block_throw() 1091 | ``` 1092 | 1093 | ## 字面量 1094 | 1095 | ### 原始字符串字面量 1096 | 1097 | C++11 提供了原始字符串字面量的写法,可以在一个字符串前方使用 `R` 来修饰这个字符串, 同时,将原始字符串使用括号包裹。 1098 | 1099 | ```C++ 1100 | #include 1101 | #include 1102 | 1103 | int main() { 1104 | std::string str = R"(C:\File\To\Path)"; 1105 | std::cout << str << std::endl; 1106 | return 0; 1107 | } 1108 | ``` 1109 | 1110 | ### 自定义字面量 1111 | 1112 | C++11 引进了自定义字面量的能力,通过重载双引号后缀运算符实现。 1113 | 1114 | ```C++ 1115 | // 字符串字面量自定义必须设置如下的参数列表 1116 | std::string operator"" _wow1(const char *wow1, size_t len) { 1117 | return std::string(wow1)+"woooooooooow, amazing"; 1118 | } 1119 | 1120 | std::string operator"" _wow2 (unsigned long long i) { 1121 | return std::to_string(i)+"woooooooooow, amazing"; 1122 | } 1123 | 1124 | int main() { 1125 | auto str = "abc"_wow1; 1126 | auto num = 1_wow2; 1127 | std::cout << str << std::endl; 1128 | std::cout << num << std::endl; 1129 | return 0; 1130 | } 1131 | ``` 1132 | 1133 | 自定义字面量支持四种字面量: 1134 | 1135 | 1. 整型字面量:重载时必须使用 `unsigned long long`、`const char *`、模板字面量算符参数,在上面的代码中使用的是前者; 1136 | 2. 浮点型字面量:重载时必须使用 `long double`、`const char *`、模板字面量算符; 1137 | 3. 字符串字面量:必须使用 `(const char *, size_t)` 形式的参数表; 1138 | 4. 字符字面量:参数只能是 `char`, `wchar_t`, `char16_t`, `char32_t` 这几种类型。 1139 | 1140 | # Reference 1141 | 1142 | [欧长坤 Modern C++ Tutorial](https://github.com/changkun/modern-cpp-tutorial)(内含很多错误0.0)(如果欲深入学习建议另寻他道) 1143 | 1144 | # 鸣谢 1145 | 1146 | 刘雪枫学长通读了讲义初版,纠正了其中**大量**错误。 1147 | --------------------------------------------------------------------------------