include 后面使用双引号""和尖括号<>的区别,以及查找路径的异同

  • #include "名字"
    优先在当前源文件所在目录(准确说是"发出此include的那个文件的目录")查找;查找不到再去bianyiqi配置的其他目录继续查找
    • 常用于项目内自己的头文件
  • #include <名字>
    直接去编译期的系统/第三方头文件目录查找,不会先看当前文件目录
    • 常用于标准库或已安装到系统的第三方库的头文件
  • 一句话经验:项目内用 " ",系统或第三方库用 < >
    不同编译器略有差异,但通用思路是:
  • "...":当前文件目录 →(用户指定的包含目录)→ 系统目录
  • <...>: (用户指定的包含目录 见具体编译器)→ 系统目录

什么是指针,常量指针和指针常量的区别,const有什么优点?

  • 在C/C++中,指针本质上存储的是一块内存的地址,它还有类型信息和解引用能力,因此可以把指针看作带类型的地址变量:
    • 它的值是内存地址;
    • 它知道自己指向什么类型
    • 它可以通过解引用*直接访问那块内存
  • 在C/C++中,"常量指针"和"指针常量"的名字很像,但可以通过修改权限来区分
    • 常量指针
      • const int *p //或 int const *p;
        • 含义: p是一个指针变量,可以改变它所指向的地址
        • 但通过p不能修改它指向的值。
      • 举例说明
          int x = 10, y = 20;
          const int *p = &x;
          p  = &y; //允许: 可以改变指向的地址
          *p = 30; // 错误: 不能修改值
          ```
        
      • 如何理解记忆: 常量指针 常量不能变,const 修饰的是*p (即指针所指的值)
    • 指针常量
      • int *const p = &x;
        • p 是一个常量指针,本身地址不能改变。
        • 但通过p可以修改它指向的值
      • 举例说明
          int x =10, y =20;
          int *const p = &x;
          *p = 30; // 允许:能修改指向的值
          p = &y; // 错误:指针本身不可改变
          ```
        
      • 如何理解记忆:指针常量, 指针不变,即地址不变。const 紧跟指针名p,说明p自身是常量
    • 两者结合const int *const p = &x; 指针和常量都不变。
  • 在C/C++中,const是一个非常重要的关键字,有很多优点
    • 保护数据不被意外修改

      • 如果变量本身应该是只读的,用const声明可以防止意外的写入
         const double PI = 3.14159
         PI = 3.14; //会编译错误
        
    • 指针参数保护

      • 在函数参数中,如果只读访问某数据,可以用const修饰,避免函数内部误修改。
      • void printStr(const char *str); //str 函数内部不会被改动,不然报错
    • 方便接口设计,让意图更清晰

      • const是一种自带文档:看到const就知道"这是只读的"。接口函数如果明确用const,调用者不用担心内部会改变传入数据,更符合"接口与实现分离"的原则
    • 有利于编译期优化和性能提升

      • 编译期常量折叠:编译器可以把const变量直接替换为字面量,减少内存读取
      • 寄存器优化:某些 const 变量可能直接放在寄存器里,不必反复访问内存。
        const int size = 10;
        int arr[size];  // ✅ 编译期就能确定大小
        
        
    • #define 相比的优势

      • #define 仅仅是预处理器文本替换,没有类型检查。
      • const真正的有类型变量,具有作用域和调试信息。

介绍下C++11的新特性

  1. 初始化列表(Initializer List)
    • 作用:支持统一的花括号 {} 初始化语法。

    • 优势:让数组、容器、对象的初始化更简洁、更安全。

      int arr[3]{1,2,3};
      std::vector<int>v{1,2,3,4};
      struct Point {int x; int y;}
      Point p{10,20}
      
    • 避免了传统 () 初始化的类型缩窄问题(narrowing)。narrowing(类型缩窄)**就是可能导致数据丢失或溢出的类型转换。**C++11 的 {} 统一初始化会在编译期禁止这种不安全转换,比传统 ()= 初始化更安全。

    • 举例

      double d = 3.14;
      int x(d);     // 传统 () 初始化
      int y = d;    // 传统 = 初始化
      
    • 这里 d 是 double,转成 int 会丢掉小数部分。 编译器一般只是警告直接截断成 3,而程序继续编译运行,可能埋下 bug。

    • 使用int x{3.14}; // ❌ 编译错误:从 double 到 int 缩窄 ,编译器会直接拒绝编译,因为存在缩窄转换风险。


  1. auto 关键字

    作用:可以这样理解,auto相当于一个占位符,让编译器根据等号有边的自动推导变量类型。
    优势:减少重复书写,尤其是长而复杂的类型。

    auto x = 10;        // 推导为 int
    vector<int>vec = {1,2,3,4};
    auto it = v.begin(); // 推导为 std::vector<int>::iterator
    

    另外 使用auto时必须对变量进行初始化,使用auto也可以定义多个变量,但必须注意,多个变量推导的结果必须为相同类型;

    auto a; //错误,没有初始化
    int a = 2;
    auto a =2
    auto *p = &a, b = 4; // &a 为int*类型,因此auto推导的结果是int类型,b也是int类型
    auto *p = &a, b = 4.5 //错误,auto推导的结果为int类型,而b推导为double类型,存在二义性
    
    • 配合 decltype 可以写出更简洁、可维护的模板代码。
    • auto使用限制
      • auto 定义变量时必须初始化
      • auto 不能在函数的参数中使用
      • auto不能定义数组,例如:auto arr[] = “abc”, (auto arr = “abc” 这样是可以的,但arr不是数组,而是指针)
      • auto 不能用于类的非静态成员变量中

  1. decltype 关键字

    作用:推导一个表达式的类型。
    用途:常用于模板或需要保持表达式类型一致的场合。

    // decltype(exp表达式)变量名 [=初始值];// []表示可选
    int a = 5;
    decltype(a) b = 10; // b 的类型与 a 相同,即 int
    

    decltype 的使用遵循以下3条规则:
    ①若exp是一个不被括号()包围的表达式,或者是单独的变量,其推导的类型将和表达式本身的类型一致
    ②若exp是函数调用,则 decltype(exp)的类型将和函数返回值类型一致
    ③若exp是一个左值,或者是一个被括号()包围的值,那么 decltype(exp)的类型将是exp的引用

    class Base{
    public:
    	int m;
    };
    
    int fun(int a, int b){
    	return a+b;
    }
    
    int main(){
    	int x = 2;
    	decltype(x) y = x //y类型为int,符合①
    	decltype(fun(x,y)) sum //sum的类型为函数fun()的返回类型,符合②
    	
    	Base A;
    	decltype(A.m) a = 0; //a的类型为int
    	decltype(A.m) b = a; //exp由括号包围,b的类型为int&,符合③
    	
    	decltype(x+y)c = 0; //c 的类型为int
    	decltype(x=x+y)d = c;//exp为左值,则d的类型为int&,符合③
    	return 0;
    }
    
    

    decltype 和 auto 的区别:(两者都可以推导出变量的类型)
    • auto 是根据等号右边的初始值推导出变量的类型,且变量必须初始化,auto的使用更加简洁
    • decltype 是根据表达式推导出变量的类型,不要求初始化,decltype 的使用更加灵活


  1. 范围 for 循环(Range-based for)

    作用:简化容器或数组的遍历。

    std::vector<int> v{1,2,3};
    // 使用冒号(:)来表示从属关系,前者是后者中的一个元素,for循环依次遍历每个元素,auto自动推导为int类型
    for (auto &x : v) {
        x *= 2;
        cout << x << endl;
    }
    
    • 代码更简洁,不再需要 begin()end() 或索引下标。

  1. nullptr 关键字

    作用:提供类型安全的空指针常量。
    优势:替代 NULL0,避免二义性。

    void f(int);
    void f(char*);
    f(nullptr);  // 调用 f(char*),不会与 f(int) 混淆
    

    nullptr是一种特殊类型的字面值,可以被转换成任意其他的指针类型,也可以初始化一个空指针。


  1. Lambda 表达式

    lambda表达式定义来一个匿名函数,一个lambda具有一个返回类型,一个参数列表和一个函数体。与函数不同的是,lambda表达式可以定义在函数类别,其格式如下:

    [capture list](parameter list) -> return type{function body}
    // [捕获列表](参数列表)-> 返回类型{函数体}
    

    作用:在需要的地方直接定义匿名函数。
    优势:更易写出简洁的回调、算法逻辑。

    • capture list(捕获列表):定义局部变量的列表(通常为空)

    • parameter list(参数列表)、return type(返回类型)、function body(函数体)和普通函数一样

    • 可以忽略参数列表和返回类型,但必须包括辅获列表和酒数体

      auto add = [](int x, int y) { return x + y; };
      int r = add(2, 3);
      
      std::for_each(v.begin(), v.end(), [](int &x){ x *= 2; });
      
    • 支持捕获外部变量 [=](按值)、[&](按引用)举例。

      	#include <iostream>
      	#include <vector>
      	using namespace std;
      	
      	int main() {
      	    int a = 10, b = 20;
      	
      	    // 使用按值捕获 [=]
      	    auto lambda = [=]() {
      	        cout << "a: " << a << ", b: " << b << endl;
      	        // a 和 b 是按值捕获的,不会被修改
      	    };
      	
      	    // 调用 lambda
      	    lambda();  // 输出 a: 10, b: 20
      	
      	    // 修改外部变量
      	    a = 100;
      	    b = 200;
      	
      	    // 再次调用 lambda
      	    lambda();  // 仍然输出 a: 10, b: 20,因为按值捕获的副本不受外部修改影响
      	}
      
      #include <iostream>
      #include <vector>
      using namespace std;
      
      int main() {
          int a = 10, b = 20;
      
          // 使用按引用捕获 [&]
          auto lambda = [&]() {
              cout << "a: " << a << ", b: " << b << endl;
              a *= 2;  // 修改 a
              b += 5;  // 修改 b
          };
      
          // 调用 lambda
          lambda();  // 输出 a: 10, b: 20
          cout << "After lambda call, a: " << a << ", b: " << b << endl;
      
          // 再次调用 lambda
          lambda();  // 输出 a: 20, b: 25
          cout << "After second lambda call, a: " << a << ", b: " << b << endl;
      }
      
      

  1. 智能指针(Smart Pointer)

    作用:自动管理动态内存,防止内存泄漏。
    主要类型

    • std::unique_ptr:独占所有权。
      • 同一时间只能有一个智能指针可以指向这个对象,但之所以说使用unique_ptr 智能指针更加安全,是因为它相比于 auto_ptr 而言禁止了拷贝操作,unique_ptr 采用了移动赋值 std::move()函数来进行控制权的转移。
    • std::shared_ptr:共享所有权,引用计数。
      • 共享指针 share_ptr是一种可以共享所有权的智能指针,定义在头文件 memory 中,它允许多个智能指针指向同一个对象,并使用引用计数的方式来管理指向对象的指针(成员函数use_count(可以获得引用计数),该对象和其相关资源会在“最后一个引用被销毁”时候释放。share_ptr是为了解决 auto_ptr 在对象所有权上的局限性(auto_ptr 是独占的),在使用引用计数的机制上提供了可以共享所有权的智能指针。
      • 循环引用: 当两个 shared_ptr 互相引用时,它们的引用计数不会减少为 0,因为它们彼此持有对方的引用。这种情况通常发生在数据结构中,例如图或双向链表,导致内存无法释放,从而发生内存泄漏。
    • std::weak_ptr:弱引用,不增加计数。
      • weak_ptr 弱指针是一种不控制对象生命周期的智能指针,它指向一个 share-ptr 管理的对象,进行该对象的内存管理的是那个强引用的share_ptr,也就是说weak_ptr不会修改引用计数,只是提供了一种访问其管理对象的手段,这也是它称为弱指针的原因所在.此外,weak_ptrshare_ptr 之间可以相互转化,share_ptr 可以直接赋值给weak_ptr,而weak_ptr 可以通过调用lock 成员函数来获得share_ptrweak_ptr 主要用于防止循环引用的问题。
      #include <iostream>
      #include <memory>
      
      class A;  // 前向声明
      class B;  // 前向声明
      
      class A {
      public:
         std::shared_ptr<B> b;  // A 持有 B 的 shared_ptr
         ~A() { std::cout << "A 被销毁\n"; }
      };
      
      class B {
      public:
         std::weak_ptr<A> a;  // 使用 weak_ptr 来避免循环引用
         ~B() { std::cout << "B 被销毁\n"; }
      };
      
      int main() {
         std::shared_ptr<A> a = std::make_shared<A>();
         std::shared_ptr<B> b = std::make_shared<B>();
         
         a->b = b;  // A 持有 B
         b->a = a;  // B 使用 weak_ptr 来观察 A,避免循环引用
      
         // 使用 weak_ptr 的 lock() 来访问 A
         if (auto lockedA = b->a.lock()) {
             // 如果 lock 成功,lockedA 将是有效的 shared_ptr
             std::cout << "成功锁定 A 对象\n";
         } else {
             std::cout << "A 对象已经被销毁\n";
         }
      
         // 此时 A 和 B 都会被销毁,内存不会泄漏
      }
      

  1. 左值与右值(Lvalue && Rvalue)

    左值(Lvalue)

    定义:左值表示内存中有一个具体的地址,它代表的是可以被修改的数据对象,通常是变量、数组元素、解引用指针等。

    特点:

    • 左值有明确的内存地址,可以在赋值语句的左边出现(这就是“左值”这个名字的由来)。
    • 左值可以被修改(非 const),可以对其进行赋值操作。
    • 可以通过引用来访问。
      举例:
    int x = 10;     // x 是左值
    x = 20;         // 可以给左值赋值
    int* p = &x;    // 左值的地址存储在指针 p 中
    int arr[3] = {1, 2, 3}; // arr[0] 是左值
    arr[0] = 10;    // 可以修改
    

    右值(Rvalue)
    定义:右值是一个没有明确内存地址的临时对象,它代表的是无法直接修改的数据,通常是字面常量、临时对象、表达式的结果等。

    特点

    • 右值没有持久的内存地址,通常是临时的、短期存在的对象。
    • 右值不能出现在赋值语句的左边(即不能作为目标变量),但可以作为右边的值进行赋值。
    • 右值常常用于移动语义(如在移动构造函数中)。
      举例:
    int x = 10;
    int y = x + 5;   // x + 5 是右值
    int z = 20;      // 20 是右值
    

    右值的具体类型:

    1. 纯右值(Prvalue):表示临时对象的结果,通常是常量或表达式的计算结果。
      • 例子:5, x + y, 临时返回值等。
    int x = 10;
    int y = 5 + x;   // 5 + x 是右值(临时计算结果)
    
    std::vector<int> createVector() {
        std::vector<int> v = {1, 2, 3};
        return v;  // v 是右值,临时返回值
    }
    
    
    1. 将亡值(Xvalue):指的是某些将要被销毁的对象(比如通过 std::move 转换的对象)。它们是一个右值,但与纯右值不同,它们通常涉及资源的转移或清理。
      • 例子:std::move(a) 转换后的对象。
    • 作用:支持移动语义和完美转发,提高性能。
    std::vector<int> v1{1,2,3};
    std::vector<int> v2 = std::move(v1); // 资源转移,避免深拷贝
    
    • std::move(v1) 中,v1 变成了一个将亡值,意思是 v1 资源(如内存、元素等)会被转移到 v2,而不是拷贝。
    • std::move(v1) 是通过强制类型转换v1 转换为右值引用(T&&)。它并没有实际的“移动”数据,而是标记 v1 为可以移动的状态。
    1. 左值引用与右值引用
      • 左值引用(Lvalue Reference):通过 & 实现,绑定到一个左值(可以是变量或可以修改的对象)。
      • int a = 10; int& ref = a; // 左值引用
      • 右值引用(Rvalue Reference):通过 && 实现,绑定到一个右值,通常用于支持移动语义。
      • int&& rref = 5 + 2; // 右值引用绑定到右值
        右值引用主要是配合移动构造函数和移动赋值运算符,可以大幅减少拷贝,提高效率。

希望对你有帮助,未完待续

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐