C++
-
定义头文件
#ifndef __COMPLEX__ // 这样定义头文件,可以解决有些cpp在包含头文件需要的次序要求;也不会有重复的定义头文件 #define __COMPLEX__ #endif
#pragma once 是 C++ 预处理器指令,用于防止头文件被多次包含,从而避免头文件中的内容被重复定义。
-
模板
// 首先告诉编译器T(其他也行)是一个模板 template<typename T> // 然后在类中创建变量时用T T re, im; // 使用时在指定类型 complex<int> c1();
-
inline函数
// inline函数称为内联函数 inline int add(int a, int b){ return a + b}; // 用关键字inline定义内联函数 // 内联函数的特点是在每次调用时,不会产生实际的函数调用,而是将函数的代码插入到调用点处。 // 这样可以减少函数调用的开销,提高程序的执行速度 // 较小的函数和频繁调用的函数更有可能被内联(由编译器绝对是否将其内联)
-
构造函数
class complex { public: complex(double r = 0, double i = 0) : re(r), im(i) // 初值列表,默认实参,在构造一个类时没有赋初值就用 {} // 其默认实参 private: double re, im; friend complex& __doapl (complex*, const complex&); }; int main() { complex c1(2, 1); complex c2; complex* p = new complex(4); }
-
singleton
Singleton 是一种设计模式,用于确保一个类只有一个实例,并提供一个全局访问点来访问该实例。这通常用于需要全局唯一性的情况,例如配置管理、数据库连接、日志记录器等
// 把构造函数写到private中 class Singleton{ public: static Singleton& getInstance() { static Singleton instance; return instance; } private: Singleton(){} // 私有构造函数,防止外部实例化 Singleton(const Singleton&) = delete; }
-
类中const
// 这种在函数名后加入const,通常是在类中的成员函数使用,表示不能修改类中的数据成员,如上面,不能修改类中定义的成员变量。 // 同时调用时,一般时要创建const 类 名字;创建const类对象才能调用const函数 // 但是,如果在类中的变量前加入mutable关键字,即可改变
-
引用传递
引用传递类似C语言中的指针传递,会增加传递的速度。
// 函数通过引用传递修改变量的值 // 同时在函数中也会修改变量,这时可以加入const防止修改变量的值 eg: const int&; void modifyValue(int &num) { num *= 2; } int main() { int x = 5; std::cout << "初始值 x = " << x << std::endl; modifyValue(x); std::cout << "修改后 x = " << x << std::endl; return 0; }
-
friends
相同class的各个objects互为friends(友元)
-
运算符重载
class Vector { public: int x, y; Vector(int x_val, int y_val) : x(x_val), y(y_val) {} // 重载 + 运算符 Vector operator+(const Vector &other) const { return Vector(x + other.x, y + other.y); // 运算符重载会创建临时对象来存储相加的结果 } }; int main() { Vector v1(1, 2); Vector v2(3, 4); Vector result = v1 + v2; // 运算符重载会创建临时对象来存储相加结果 return 0; }
-
使用引用传递:
- 传递大型对象: 当函数需要修改或使用大型对象时,使用引用传递通常更高效,因为它避免了对象的复制。这可以减少内存和性能开销。
- 修改传递的参数: 如果函数需要修改传递的参数的值,必须使用引用传递。在函数内部对引用参数的修改会影响到调用函数时传递的变量。
- 避免不必要的复制: 对于对象或数据结构,如果你不想在函数中创建副本,可以使用引用传递。
- 避免大量数据的复制: 在处理容器(如
std::vector
、std::string
等)时,使用引用传递可以避免不必要的大量数据复制。
-
使用传值:
- 函数不需要修改参数值: 如果函数不需要修改传递的参数值,可以使用传值,以保持函数对传递数据的不可变性。
- 基本数据类型和小型结构: 对于基本数据类型(如整数、浮点数等)和小型结构,传值可能更简单且不会引入大量的性能开销。
- 需要独立副本: 有时,函数可能需要在操作期间使用独立的数据副本,以确保不会影响调用函数时的原始数据。
- 避免引用可能为空的情况: 如果函数的参数可能为空指针或无效引用,传值可以避免讨论引用的有效性。
-
使用引用作为函数的返回值:
- 避免数据复制: 使用传引用可以避免对复杂对象或数据结构的不必要数据复制,从而提高性能和效率。
- 返回对象的一部分: 如果函数返回的是对象的一部分,例如容器中的一个元素,传引用可能更合适,因为它避免了复制整个对象。
- 返回类的成员: 当函数返回的是类的成员变量时,传引用可以避免复制该成员,而是直接返回对成员的引用。
-
不能用引用作为函数的返回值
- 返回临时对象: 如果函数返回的是临时创建的对象,尤其是在函数内部创建的对象,传引用可能不合适,因为临时对象的生命周期可能短暂。
- 返回局部变量的引用: 不要返回函数内部局部变量的引用,因为函数结束后局部变量会被销毁,返回的引用将指向无效的内存。
- 返回非引用类型: 如果函数的目标是返回一个值,而不是引用,那么使用传值作为返回类型可能更合适。引用传递可能导致与用户预期不符的行为
int function() { int x = 42; return x; // Returns a copy of the value of x } int result = function(); // result receives a copy of the value (42) /* 上面函数的返回类型是值传递类型,调用function函数得到的只是返回值的copy,这样改变result,也不会改变返回值。 */ int globalValue = 42; int& function() { return globalValue; // Returns a reference to globalValue } int& resultRef = function(); // resultRef is a reference to globalValue resultRef = 100; // Modifies the original globalValue to 100 /* 这个是使用引用作为函数的返回值,调用function函数后,如果修改resultRef的值,那么globalValue函数也会改变。同时注 意的是,这里使用引用作为返回值,返回的不能是函数内部定义的值,因为函数定义内部的值会随着函数的调用解释后销毁。 */
-
返回值类中定义
inline complex operator + (const complex& x, const complex& y) { return complex ( real (x) + real(y),imag(x) + imag(y)); }
-
拷贝构造、赋值、析构
class String { public: String(const char* cstr = 0); // 构造函数 String(const String& str); // 拷贝构造 String& operator=(const String& str); // 拷贝赋值 ~String(); // 析构函数 char* get_c_str() const { return m_data; }; private: char* m_data; }
-
浅拷贝
![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/3f9ed2b554284253bd9d2f74b88c6288.png#pic_center)直接赋值,会造成a指针指向的空间 赋给了b指针指向的空间,但是b指针指向的内容就没有指针指向,会造成内存泄漏
换种理解,在析构时,析构a的时候就释放了hello的内存,b在析构时,再析构一次,就会造成重复释放。
-
深拷贝
![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/328b112435fa4e068925515eab241327.png#pic_center) -
生命周期
static object,其生命在作用域结束之后仍然存在,直至整个程序结束
global object, 其生命在整个程序结束之后才结束,可以视作为一种static object,其作用域是整个程序
heap object, 生命在它被deleted之后结束,如果没有delete,那么当作用域结束后,指针随着结束,但是heap object仍然存在,会造成内存泄漏
-
new
Complex* pc = new Complex(1, 2); // 等同于 void* mem = operator new (sizeof(Complex)); // 分配内存,operator new等同于调用malloc函数 pc = static_cast<Complex*>(mem); // 将men的void类型转换为Complex*的类型 pc->Complex::Complex(1, 2); // 构造函数 等同于Complex( this, 1, 2);
-
delete
// 先调用dtor,再释放memory Complex* pc = new Complex(1, 2); delete pc; // 编译器转化为 Complex:: ~Complex(pc); // 析构函数 operator delete(pc); // 释放内存 free(pc)
-
内存分配
长条属于debug版本的的内存分配空间,Complex上下是debug版本加的head,Complex是8个字节,每个空格是4个字节,00000041是cookie,是malloc和delete约定好的内存大小,原本的内存大小是52,所以要对其进行填充,(pad)就是填充为补齐16倍数的空间,再vc中内存空间一般为16的倍数,补齐为64, 64的16进制为0x40,变为0x41后面的1表示为操作系统分配出内存,短条属于release版本。
-
array
-
static
c1.real()相当于C语言real(&c1),把c1地址传进去,由this指针接受 类中的成员函数,有个隐藏的参数是this指针,有编译器提供,不能自己写上去;可以在函数中用它 没有static的成员函数,创建对象时,每个对象都由this指针来创建指向 staic成员函数,创建时,会和对象脱离了, staic数据,只会有一份。 **staic成员函数和非静态成员函数区别在于没有this指针,不能处理非静态的对象和数据;只能处理静态数据**
![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/af098aede83a40578dc5996ee8ebb462.png#pic_center)静态方法(静态成员函数)可以调用非静态变量,但前提是你必须在静态方法中有一个对象的实例或者对象的指针,然后通过这个实例或指针来访问非静态变量。这是因为非静态变量是与类的实例相关联的,而静态方法本身不依赖于任何特定实例 #include <iostream> class MyClass { public: int nonStaticVar; // 非静态成员变量 static void staticMethodWithInstance(MyClass& obj) { // 静态方法中使用对象实例访问非静态变量 std::cout << "Static method called with nonStaticVar = " << obj.nonStaticVar << std::endl; } }; int main() { MyClass obj; obj.nonStaticVar = 42; // 初始化非静态变量 // 通过对象实例调用静态方法,访问非静态变量 MyClass::staticMethodWithInstance(obj); return 0; }
在private中构造个自己a,static只有一份,外界构造不了,在使用时,用A::getInstance().setup()来使用,就是调用A作用域下的getInstance函数,返回a,再用.运算符来调用a对象的函数。
![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/b858f2fec2dc41df87600dce256b718f.png#pic_center)这是更好的写法,将static对象a放到类外,这是为了让万一没有使用到这个对象时,就不会产生该对象,避免浪费,这个static关键字用到C语言中一个特性,即如果没有用到该对象,就会视作为不存在,用到了才会产生对象
-
delegate
-
函数指针委托
typedef void (*SaveFunction)(const std::string&); /* 这是定义一个函数指针取名(SaveFunction),指针指向的为 void (const std::string&)类型的函数, 然后可以根据需求,写出多个需要的该类型的函数, 后续调用时,可以定义出一个函数指针,需要的话,就将该函数指针指向函数,进行调用*/ SaveFunction saveFunction = nullptr; saveFunction = SaveAsPlainText; saveFunction(textToSave);
-
-
智能指针
智能指针是一种用于管理动态分配的内存的C++特性,可以自动处理内存的分配和释放,有助于避免内存泄漏和资源管理问题。
1.shared_ptr
std::shared_ptr
允许多个智能指针共享同一个堆上分配的对象。它使用引用计数来跟踪有多少智能指针共享同一对象。当最后一个指向对象的std::shared_ptr
被销毁时,对象的内存会自动释放。template<class T> class shared_ptr { public: T& operator*() const { return *px; } T* operator->() const {return px;} shared_ptr(T* p) : px(p) {} private: T* px; long* pn; }; struct Foo { ... void method(void){ ... } }; shared_ptr<Foo> sp(new Foo); Foo f(*sp); sp->method(); // px->method();
2.std::unique_ptr
std::unique_ptr
代表独占所有权的智能指针。一个对象只能有一个std::unique_ptr
持有。当std::unique_ptr
被销毁或转移所有权时,对象的内存会自动释放。3.std::weak_ptr
std::weak_ptr
是一种弱引用智能指针,它可以观察std::shared_ptr
所管理的对象,但不会增加引用计数。它通常用于解决std::shared_ptr
循环引用问题。 -
编译过程
编译:指在程序的构建阶段,源代码被翻译成计算机可以执行的机器代码;源代码经过编译器的处理,检查语法错误、语义错误和警告,并生成中间代码或目标代码。
运行:运行时是指在程序被加载到内存并执行时的阶段;计算机的中央处理单元(CPU)根据可执行文件中的机器代码执行程序的指令,程序可以与外部数据、用户输入和其他进程进行交互,并执行各种计算和操作,运行时的错误包括程序崩溃、内存溢出、除零错误等,它们在程序执行时发生。
-
源代码到可执行文件的过程
- 编写源代码:程序员使用文本编辑器编写源代码,这些源代码包含了程序的逻辑和功能。
- 预处理(Preprocessing):在编译之前,源代码经过预处理。预处理器执行一些特定的任务,如宏展开、头文件包含、条件编译等。结果是一个经过处理的源代码文件。
- 编译(Compilation):编译器接收经过预处理的源代码并将其转换成汇编语言或二进制机器代码。编译器会检查语法错误和警告,并生成中间文件或目标文件。
- 链接(Linking):如果程序由多个源文件组成,编译后会生成多个目标文件。链接器将这些目标文件合并成一个可执行文件。它还负责解析函数和变量的引用,确保它们正确连接在一起。
- 可执行文件生成(Executable Generation):链接器生成最终的可执行文件,该文件包含了程序的所有机器代码。这是可以在计算机上运行的二进制文件。
- 运行时执行(Runtime Execution):用户在计算机上运行可执行文件。操作系统将可执行文件加载到内存中,并从
main
函数开始执行。程序在运行时与系统和用户进行交互,并执行其功能。
-
constexpr常量和表达式
用于声明常量表达式(Constant Expressions)。常量表达式是在编译期间计算出结果的表达式,它可以用于在编译时执行某些操作,而不是在运行时执行。
constexpr的使用有一些限制,例如,
constexpr
函数必须是确定性的(不依赖于运行时输入),并且表达式必须能够在编译期间计算。 -
decltype
decltype(expression) variable_name;
其中,
expression
可以是任何有效的 C++ 表达式,包括函数调用、变量名等。variable_name
是一个用来存储expression
类型的变量或声明。decltype
不会实际执行表达式,而只是在编译时分析表达式的类型。int x = 42; decltype(x) y; // 声明一个与 x 类型相同的变量 y const int& ref = x; decltype(ref) z = x; // 声明一个与 ref 类型相同的变量 z int add(int a, int b) { return a + b; } decltype(add(1, 2)) result; // 声明一个与 add(1, 2) 的返回类型相同的变量 result
-
static_cast类型转换
new_type value = static_cast<new_type>(expression); new_type 是要将表达式转换为的新数据类型。 expression 是要进行转换的表达式或值。 int integerNumber = 42; double doubleNumber = static_cast<double>(integerNumber); Base* basePtr = new Derived(); Derived* derivedPtr = static_cast<Derived*>(basePtr); class Base { /* ... */ }; class Derived : public Base { /* ... */ }; Base* basePtr = new Derived(); Derived* derivedPtr = static_cast<Derived*>(basePtr); 这种转换只在确保 basePtr 指向的对象实际上是 Derived 类型的情况下才能安全地执行。
-
const_cast修改const
用于在编译时删除对象的
const
限定符或volatile
限定符。const int constValue = 42; int nonConstValue = const_cast<int>(constValue);
volatile int volatileValue = 42; int nonVolatileValue = const_cast<int>(volatileValue);
-
范围for语句
范围for语句(Range-based for loop)是C++11引入的一种循环结构,用于遍历容器(如数组、向量、列表等)或其他支持迭代器(iterators)的数据结构。它提供了一种更加简洁和直观的方式来遍历容器中的元素,无需使用传统的索引或迭代器。
int main() { std::vector<int> numbers = {1, 2, 3, 4, 5}; // 使用范围for遍历vector中的元素 // num还可以用引用类型 &num,会修改原容器的值 for (int num : numbers) { std::cout << num << " "; } return 0; }
-
throw语句
#include <iostream> #include <stdexcept> // 自定义异常类 class MyException : public std::exception { public: MyException(const char* message) : message_(message) {} const char* what() const noexcept override { return message_; } private: const char* message_; }; int divide(int numerator, int denominator) { if (denominator == 0) { // 使用 throw 引发自定义异常 MyException throw MyException("Division by zero"); } return numerator / denominator; } int main() { try { int result = divide(10, 0); // 引发异常 std::cout << "Result: " << result << std::endl; } catch (const MyException& e) { // 捕获并处理自定义异常 std::cerr << "Exception caught: " << e.what() << std::endl; } return 0; }
#include <iostream> #include <stdexcept> #include <string> // 自定义异常类 class MyException1 : public std::exception { public: MyException1(const char* message) : message_(message) {} const char* what() const noexcept override { return message_; } private: const char* message_; }; class MyException2 : public std::exception { public: MyException2(const char* message) : message_(message) {} const char* what() const noexcept override { return message_; } private: const char* message_; }; int divide(int numerator, int denominator) { if (denominator == 0) { // 使用 throw 引发自定义异常 MyException1 throw MyException1("Division by zero"); } if (numerator < 0) { // 使用 throw 引发自定义异常 MyException2 throw MyException2("Negative numerator"); } return numerator / denominator; } int main() { try { int result1 = divide(10, 0); // 引发 MyException1 int result2 = divide(-5, 2); // 引发 MyException2 std::cout << "Result 1: " << result1 << std::endl; std::cout << "Result 2: " << result2 << std::endl; } catch (const MyException1& e) { // 捕获并处理 MyException1 类型的异常 std::cerr << "Exception caught (MyException1): " << e.what() << std::endl; } catch (const MyException2& e) { // 捕获并处理 MyException2 类型的异常 std::cerr << "Exception caught (MyException2): " << e.what() << std::endl; } catch (const std::exception& e) { // 捕获其他标准异常类型的异常 std::cerr << "Standard exception caught: " << e.what() << std::endl; } catch (...) { // 捕获其他未知类型的异常 std::cerr << "Unknown exception caught" << std::endl; } return 0; }
-
默认实参
默认实参(Default Arguments)是C++中的一项特性,它允许您在函数声明中为一个或多个函数参数指定默认值。如果调用函数时没有提供相应参数的值,那么函数将使用默认值执行。
// 函数声明中指定默认实参 void printMessage(const std::string& message = "Hello, World!"); int main() { // 调用函数时没有提供参数值,将使用默认值 printMessage(); // 输出: Hello, World! // 调用函数时提供参数值,将覆盖默认值 printMessage("Goodbye!"); // 输出: Goodbye! return 0; } // 函数定义,可以与声明一致或不一致 void printMessage(const std::string& message) { std::cout << message << std::endl; }
-
智能指针shared_ptr
// 需要包含的头文件:#include <memory> /* 用于管理动态分配的堆内存。它提供了一种共享所有权的方式,允许多个智能指针共同拥有同一块堆内存,并在不再需要时自动释放该内存。 */
// 使用构造函数初始化 std::shared_ptr<int> p1(new int); *p1 = 42; // 使用 make_shared 函数初始化 // std::make_shared 是创建 std::shared_ptr 的首选方式,因为它提供了更高的安全性和性能,并且代码更加简洁。只有在需 // 要自定义删除器时才考虑使用 std::shared_ptr 构造函数。 auto p2 = std::make_shared<std::string>("Hello, World!"); // 使用拷贝或赋值构造函数 std::shared_ptr<int> p3 = p1; std::shared_ptr<int> p4(p1);
// std::shared_ptr 允许多个智能指针共享同一块堆内存。当最后一个共享指针不再需要该内存时,它会自动释放内存。 std::shared_ptr<int> p1 = std::make_shared<int>(42); std::shared_ptr<int> p2 = p1; // 共享所有权 p1.reset(); // 释放内存 // 此时 p2 仍然有效
// 获取原始指针,可以使用get()成员函数来获取shared_ptr内部的原始指针 std::shared_ptr<int> p = std::make_shared<int>(42); int* rawPtr = p.get(); // 获取原始指针
注意使用shared_ptr管理动态数组:
// std::shared_ptr 也可以用于管理动态数组。要这样做,需要提供一个自定义的删除器函数,以确保在释放内存时使用 delete[] 而不是 delete。
-
->运算符和.运算符
使用 . 运算符直接访问对象的成员。 使用 -> 运算符通过指针访问对象的成员。
-
explict
// 通常用于修饰类的构造函数。它的作用是防止编译器进行隐式类型转换,确保只有明确请求的情况下才能调用构造函数。 class MyClass { public: explicit MyClass(int value) : data(value) { } void PrintValue() { std::cout << "Value: " << data << std::endl; } private: int data; }; int main() { // 使用 explicit 构造函数,必须显式提供 int 类型的参数 MyClass obj1(42); obj1.PrintValue(); // 错误示例:不能进行隐式类型转换 // MyClass obj2 = 42; // 编译错误,无法隐式转换为 MyClass return 0; }
// 如果没有explict,可以通过 MyClass obj2 = 42;创建对象 创建一个临时的 MyClass 对象,使用 42 作为参数调用构造函数,即 MyClass temp(42); 然后,将这个临时对象赋值给 obj2,这可能涉及到调用复制构造函数或移动构造函数,取决于编译器的优化 这种行为可能会导致意外的结果,特别是在类型之间存在多个可行的转换时。使用 explicit 可以防止这种隐式转换,从而提高代码的可读性和安全性。
-
重载输出运算符
class MyClass { public: MyClass(int val) : value(val) {} // 重载 << 运算符 // 第一个形参是一个非常量ostream对象的引用,因为向流写入内容会改变其状态,用引用时因为无法直接复制一个ostream对象 friend std::ostream& operator<<(std::ostream& os, const MyClass& obj) { os << "MyClass(" << obj.value << ")"; return os; }
-
左值和右值
// 左值的英文简写为“lvalue” lvalue 是“loactor value”的缩写,可意为存储在内存中、有明确存储地址(可寻址)的数据 // rvalue 译为 "read value" 指的是那些可以提供数据值的数据(不一定可以寻址,例如存储于寄存器中的数据) **右值,可以理解为是即将结束生命周期的对象** // 有名称的、可以获取到存储地址的表达式即为左值;反之则是右值。 // 变量 a、b 是变量名且通过 &a 和 &b 可以获得他们的存储地址,因此 a 和 b 都是左值;反之,字面量 5、10,它们既没有名称,也无法获取其存储地址(字面量通常存储在寄存器中,或者和代码存储在一起),因此 5、10 都是右值。 常量左值引用既可以操作左值,也可以操作右值。 int num = 10; const int &b = num; const int &c = 10;
int a=5,则 ++(a++) 前置++a,是左值,而(a++)是为5,5为右值,所以会报错