首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >C++入门-笔记(Cherno视频教程)

C++入门-笔记(Cherno视频教程)

原创
作者头像
Vaeeeee
修改2025-10-20 00:24:32
修改2025-10-20 00:24:32
5060
举报

前言

  尽管 Python 简单易用,但在许多对实时性有较高要求的场景中,仍然需要使用到C++,例如在ROS系统开发、边缘设备部署,仍需依赖 C++。因此,本文主要作为入门和学习C++过程中的笔记博客,也希望对同样想学习C++的同学有所帮助。👓


一、学习资料📖

我是跟着Cherno大佬的视频开始学习C++,他的视频有趣且非常nice!Cherno-b站视频

除了视频以外,结合以下这些笔记内容进行了进一步整合和思考:

二、基础知识

  虽然C++开发有Visual Studio这款强大的IDE,但是由于本人此前深度使用Vscode,并且环境配置起来也相对比较简单,因此选择了Vscode+wsl2的方式进行C++环境的配置,并且最终使用SSH连接到wsl2中,使用起来也非常方便。

环境配置教程:wsl2+Vscode中配置C++开发环境

编译:将cpp文件变成obj文件

几个预编译指令:

● #include <XX>:实际上就是将后面文件的内容粘贴到文件中。例如:

假设定义一个x.h文件,里面内容只有一个},运行下面代码不会报错:

● #define XX YY 宏替换,就是将XX 替换成YY

● #if 表达式

.....

#endif 满足条件时才进行编译

连接:将编译后的目标文件变成可执行程序

关键字

const

  • 声明常量,一旦初始化后就不能再修改其值
代码语言:cpp
复制
const int MAX_AGE = 10;
const int*age = new int;    // 同 int const* age = new int; 功能一致
age = &MAX_AGE;
cout << *age << endl; //输出结果10

*age = 10   //不允许,会报错!!
// 原因:const int* age 中的 const 修饰的是 int 类型的值,
// 而不是指针本身,所以指针指向的值不能被修改,指针所指的地址可以改变


// ——————————————————————————————————————————————————————————————
const int MAX_AGE = 10;
int* const age = new int;
*age = MAX_AGE;
cout << *age << endl; //输出结果10
age = MAX_AGE; //会报错
  • 放在类中函数中的参数列表后面,表示该函数不会修改类中的变量,相当于拥有“只读”功能。 如果类中没有在函数的后面加上 const ,可以对m_X这个重新赋值;反之,则报错:

如果类中没有在函数的后面加上 const ,声明的静态类无法调用这个方法,反之则可以。

mutalble:声明后可以在类的const函数里改变类的成员变量值

  • 对于上面的例子,如果想要在调试时查看 GetX 这个函数调用的次数,可以增加一个整型变量,且声明mutable,否则在函数中无法修改这个变量的值。(虽然可以在 GetX 后面去掉 const 实现,但是这样的话又回导致e.GetX()这个方法报错!)——mutable使用的大部分情况

new:初始化变量,返回的是指向对象(或指向数组初始对象)的适当类型化的非零指针。在堆上分配内存

  • 如果使用了 new关键字,记住必须使用delete删除变量,释放内存。
代码语言:cpp
复制
int* a  = new int[23];
char *d = new char;
    
delete[] a;   //使用new [] 就用 delete[] 删除
delete d;

explicit:用来修饰只有一个参数的类构造函数,以表明该构造函数是显式的,而非隐式的,即禁止类对象之间的隐式转换,以及禁止隐式调用拷贝构造函数。

代码语言:cpp
复制
class Entity
{
private:
    int m_age;
    string m_X;
public:    
    Entity(int age) : m_age(age), m_X("good")
    {
    }

    Entity(string m_name)
    {
        m_X = m_name;
        m_age = 1;
    }
    const string GetName() {return m_X;}
};

// 显式调用:
    Entity x(1);
    Entity y(string("hello"));

// 隐式调用
    Entity x = 1;
    Entity y = string("hello");

// 如果在上面Entity的类中构造函数前加上 explicit,则无法使用隐式调用这种

this:一个指向当前对象实例的指针

代码语言:cpp
复制
class Entity
{
public:
    float x, y;

    Entity(float x,float y)
    {
        this->x = x;    //可以使用这种方式给x赋值
        this->y = y;
    }
};

auto:让 C++自动推导出数据的类型

● 当类型名比较长时就适合用auto,其余情况还是不要用auto,因为会导致代码可读性变差。

● 注意:auto不处理引用,所以不要漏掉&而造成一次复制

代码语言:cpp
复制
int main()
{
    std::vector<std::string> strings;
    strings.push_back("hello");
    strings.push_back("world");

    for (std::vector<std::string>::iterator it = strings.begin(); it!=strings.end(); ++it){
        std::cout << *it << std::endl;
    }

    // 使用auto关键字简化代码
    for (auto it = strings.begin(); it!=strings.end(); ++it){
        std::cout << *it << std::endl;  
    }
}
代码语言:cpp
复制
#include <iostream>
#include <string>
#include <vector>
#include <unordered_map>    

class Device{};
class DeviceManager{
private:
    std::unordered_map<std::string, std::vector<Device*>> m_Devices;
public:
    const std::unordered_map<std::string, std::vector<Device*>>& GetDevices() const{
        return m_Devices;
    }
};

int main()
{
    DeviceManager dm;
    // const std::unordered_map<std::string, std::vector<Device*>>& devices = dm.GetDevices();

    // 简化代码
    // 使用using声明
    // using DeviceManager = std::unordered_map<std::string, std::vector<Device*>>;
    // const DeviceManager& devices = dm.GetDevices();

    // 或者使用typedef
    // typedef std::unordered_map<std::string, std::vector<Device*>> DeviceMap;
    // const DeviceMap &devices = dm.GetDevices();

    // 或者使用auto
    const auto &devices = dm.GetDevices(); // 注意auto不处理引用,所以不要漏掉&而造成一次复制
    // auto devices = dm.GetDevices();  # 会复制一次
}

三、数据类型

数组:有序数据的集合

数据类型 数组名常量表达式

  • 数组实际上只是一个指针, 数组名本身就是指向第一个元素的指针
  • 给数组某一位置复制的3种方法:
代码语言:cpp
复制
// First
 int example[5];
 example[2] = 10;

// Second
 int* xx = example;
 *(xx + 2) = 20; //将指针 xx 移动2个位置(指向 example[2]),然后解引用该位置,将其值设置为 20
 
// Third
*(int*)((char*)xx +8) = 30;
// 首先将指针 xx 转换为 char* 类型。这意味着指针现在以字节(char)为单位进行指针算术。
// 然后,将指针 xx 增加8个字节(sizeof(int) 通常是4个字节,因此移动8个字节相当于移动2个 int 的位置,即 example[2] 的位置)
// 最后,将这个 char* 指针转换回 int* 指针,然后解引用,并将该位置的值设置为 30。这再次修改了 example[2] 的值

#include <vector> vector<数组中元素类型> 标识符

代码语言:cpp
复制
struct vertex
{
public:
    float x, y;

};

vector<vertex> vv;
vv.push_back({1,2});      //在数组的最后添加一个数据
vv.push_back({5,6});
vv.erase(vv.begin()+1);   //删除第二个元素

静态数组(std::array)

代码语言:cpp
复制
// 使用std::arrat
std::array<int, 5> data;
data[0] = 1;
data[4] = 2;

// C风格的普通数组
int dataOld[5];
dataOld[0] = 1;

● 静态数组和普通数组的异同

○ 相同:都是在栈上分配,不像std::vector实在堆上分配的

○ 不同:std::array有边界检查(仅在debug模式下)

● 建议开始使用std::array,因为增加了一层调试,而且没有性能成本。

字符和字符串:字符使用''。字符串使用""

指针:本质就是存储内存地址的整数

  • 一个变量的地址称为该变量的指针
  • 指针前面的类型其实是一个说明符,指针变量的数据类型用来指定该指针变量所指向数据的类型,记住指针的本质永远是 存储内存地址的整数

数据类型标识符 *指针变量名;

代码语言:cpp
复制
int i = 100;
int* ptr = &i;   //&表示获取变量的地址   但是注意:int& ref = i; 表示引用
*ptr = 5;    // 这是一个解引用操作符,用于访问指针 ptr 所指向的内存地址中存储的值。

函数指针:Function pointer是将一个函数赋值给一个变量的方法

  • 首先看一下简单的函数指针用法:
代码语言:cpp
复制
void HelloWorld(){
    std::cout<<"Hello World"<<std::endl;    
}
int main()
{
    void (*function)();
    function = HelloWorld;  
    function();
    function();

    // 使用auto
    auto fun= HelloWorld;
    fun();
    fun();
} 
/*
Hello World
Hello World
Hello World
Hello World
*/
  • 带参数的函数指针:
代码语言:cpp
复制
void HelloWorld(int a)
{
    std::cout << "HelloWorld:" << a <<  std::endl;
}
int main()
{
    typedef void(*HelloWorldFunction)(int);
    HelloWorldFunction function = HelloWorld;
    function(3);   
}
// 输出:HelloWorld:3
  • 应用:将函数作为另一个函数的参数然后调用它
代码语言:cpp
复制
#include <iostream>
#include <vector>
void PrintValue(int value){
    std::cout << value << std::endl;    
}

void ForEach(const std::vector<int>& values, void (*func)(int)){
    for (int value: values){
        func(value);
    }
}
int main()
{
    std::vector<int> values{1, 2, 3};
    ForEach(values, PrintValue);
}
/*
1
2
3
*/

引用:相当于变量的一个别名,对它的操作和原来对象的操作相同

数据类型 & 表达式;

  • 引用必须要进行赋值(所以上方写的是表达式);一个引用被初始化后,无法使用它 再去引用另一个变量;int a = 1; int b = 2; int &ref = a; ref = b; // 运行之后,a的值变成了2,而不是说ref被重新引用成了b
  • 引用的注意事项:
  • 比较下面3种不同的函数,体会函数的 值传递引用传递
代码语言:cpp
复制
void Increment(int value)
{
     value++;
}

void Increment1(int* value)
{
     (*value)++;
}

void Increment2(int& value)
{
    value++;
}
int main()
{
     int num =1;
     Increment(num);
     cout << "num is: " << num << endl;

     Increment1(&num);
     cout << "num iss:" << num << endl; 
     
     Increment2(num);
     cout << "num isss: " << num << endl;  
}
// 输出:
// num is: 1
// num iss:2
// num isss: 3

枚举类型

enum 枚举类型名 {标识符列表}

  • 默认从标识符的第一个元素赋值为0,如果在标识符中间某个元素指定赋值了,则后面自动累加赋值enum Example : char { A, B=65, C }; Example val=B, qwe = C; cout << val << qwe <<endl; //输出的值是 AB
  • 可以指定给枚举赋值的整数类型(可以是char类型,不能是float类型)

四、类和结构体

可见性:public、private、protected

● public:对外可见,对内可见;

● private:对外不可见,对内可见;

● protected:对外不可见,对内可见,且对派生类是可见的。

  • 可见性比较:
代码语言:cpp
复制
// public:类的外部也可以访问这个变量
class Father{
public:
    int x,y;
public:
    Father()
    {
        x = 10086;
    }
};

class Son:public Father
{
public:
    Son()
    {
        x = 10000;
    }
};

int main()
{
    Father f;
    Son s;
    f.x = 1;
    s.x = 2;
}

使用private声明后在子类中访问会报错,外部访问更会报错;

重载运算符:运算符实际就是一个函数,所以实际上就是函数的重载

代码语言:cpp
复制
// 实现两个实例化的类进行相加:

// 法一:定义一个相加函数

struct Vector2
{
    float x, y;

    Vector2(float x, float y) : x(x), y(y){}

    Vector2 Add(const Vector2& other) const
    {
        return Vector2(x + other.x, y + other.y);
    };

};
Vector2 position(4.0f, 1.0f);
Vector2 speed(5.0f, 3.0f);
Vector2 result = position.Add(speed);


// 法二:重载运算符
struct Vector2
{
    float x, y;

    Vector2(float x, float y) : x(x), y(y){}

    Vector2 operator+(const Vector2&other)
    {
        return Vector2 (x + other.x, y+other.y);
    };

};
Vector2 result = position + speed;

箭头操作符

  • 如果实例化的类是一个指针的话,不能像其他实例化对象可以直接使用 .XX 调用函数
  • 总结: 箭头(->):左边必须为指针; 点号(.):左边必须为实体。

构造函数:特殊的方法,在创建类的实例时运行,主要用来初始化该类

  • 对比下面两段代码:
代码语言:cpp
复制
class Qaq
{
public:
    float x, y;
    void Print()
    {
        cout << x << ", " << y << endl;
    }
};
Qaq q;
q.Print();  //输出: -1.01376e+34, 4.59163e-41 

//  现在若想要进行初始化,可以声明一个init函数,但是必须在实例化后调用这个函数才能初始化;
//  如果通过构造函数的方式实现就可以自动初始化。



// 添加构造函数
class Qaq
{
public:
    float x, y;

    Qaq()
    {,                                         
        x = 1.0f;
        y = 2.0f;
    }

    void Print()
    {
        cout << x << ", " << y << endl;
    }
};

int main()
{
Qaq q;
q.Print(); 
}  //输出: 1, 2
构造函数初始化列表

在构造函数进行初始化类成员时有两种方法:

1️⃣. 常规方法:在构造函数的内部给变量赋值

代码语言:cpp
复制
class Entity
{
private:
    string m_X;
public:    
    Entity()
    {
        m_X = "haha";
    }

    Entity(string m_name)
    {
        m_X = m_name;
    }

    const string GetName() {return m_X;}
};
int main() 
{
    Entity e1;
    cout << e1.GetName() << endl;
    Entity e2("wowo");
    cout << e2.GetName() << endl;
}
/*
输出:
haha
wowo
*/

2️⃣. 在构造函数后面使用这种形式:❤(优先选择这种方式去初始化,可读性和逻辑性)

函数名 : 参数名(初始化的值)

代码语言:cpp
复制
class Entity
{
private:
    int m_age;
    string m_X;
public:    
    Entity() : m_age(23), m_X("good")   // 变量的初始化顺序 和 定义类成员的顺序一致
    {
    }

    Entity(string m_name)
    {
        m_X = m_name;
    }

    const string GetName() {return m_X;}
};
  • 注意:初始化时要按照定义类成员的顺序进行初始化,否则会导致其他依赖问题!
拷贝构造函数(复制构造函数)
  • 比较下面两种复制值的方式:

在类对象的复制过程中,实际上调用了默认的拷贝构造函数,也可以自定义拷贝构造函数:

代码语言:cpp
复制
struct  vector
{
    float x, y;

    vector(float m, float n) 
        : x(m), y(n) {}
    vector(const vector &v)
    {
        x = v.x;
        y = v.y;
        cout << "Copy constructor called" << endl;
    }
};
int main()
{
    vector a = {2,3};
    vector b =a;
}
// 终端输出:Copy constructor called
  • 📌📌手动实现 String 类(⭐⭐⭐要素过多,反复观看)
代码语言:cpp
复制
#include <iostream>
#include <string.h>
using namespace std;    

class String
{
private:
    char* m_Buffer;      //指向字符缓冲区
    unsigned int m_Size; //保存string的大小
public:
    String(const char* string) 
    {
        m_Size   = strlen(string);
        m_Buffer = new char[m_Size];
        
        // 把这个指针复制到实际的缓冲区,这样缓冲区就会被我们的字符填充
        // 法一:
        // for (int i = 0; i < m_Size; i++)
        //     m_Buffer[i] = string[i];
        // 法二:更简洁
        memcpy(m_Buffer, string, m_Size);
    }

    friend ostream &operator<<(ostream &stream, const String &string);
};

// 重载输出运算符,将 `String` 对象输出到流 `std::ostream` 中。
ostream& operator<<(ostream& stream, const String& string)
{
    stream << string.m_Buffer;
    return stream;
}
int main()
{
    String string = "Cherno";
    cout << string << endl;
    // cin.get();
}

上面代码存在几个问题:

1. 没有空终止字符,会导致出现随机字符

修改:

m_Buffer=newcharm_Size+1;//增加空终止符的位置 m_Bufferm_Size=0;//添加空终止符

2. 内存泄漏:分配的new char并没有delete

修改:

~String() { delete[] m_Buffer; }

3. 复制出错

代码语言:cpp
复制
// 如果对其进行复制,会出现以下错误:
String string = "Cherno";
String second = string;
cout << string << endl;
cout << second << endl;

出错原因:运行String second = string;时 C++自动为我们做的是它将所有类成员变量,而这些成员变量组成了类(实例的内存空间),它是由一个 char*和一个 unsigned int 组成的,它将这些值复制到了一个新的内存地址里面,这个新的内存地址包含了这个 second 字符串。

现在问题来了,内存中有两个 String,因为它们直接进行了复制,这种复制被称为shallow copy(浅拷贝)。它所做的是复制这个 char,内存中的两个 String 对象有相同的 char的值,换句话说就是有相同的内存地址。这个 m_Buffer 的内存地址,对于这两个 String 对象来说是相同的,所以程序会崩溃的原因是当我们到达作用域的尽头时,这两个 String 都被销毁了,析构函数会被调用,然后执行delete[] m_Buffer两次,程序试图两次释放同一个内存块。这就是为什么程序会崩溃——因为内存已经释放了,不再是我们的了,我们无法再次释放它。

如果修改second的某个值:second[2] = 'a';(需要设置操作符[]重载),最后会出现:

  • 总结:我们真正需要做的是,分配一个新的 char 数组,来存储复制的字符串。而我们现在做的是复制指针,两个字符串对象指向完全相同的内存缓冲区,它同时改变了它们,因为它们指向同一个内存块。或者当我们删除的时候它会把它们两个都删除,因为它们指向同一个内存块。 修改:自定义一个自己的拷贝构造函数: String(const String& other) : m_Size(other.m_Size) { m_Buffer = new charm_Size + 1; memcpy(m_Buffer, other.m_Buffer, m_Size + 1); }
  • 最终修改后的代码(手动实现string类):
代码语言:cpp
复制
#include <iostream>
#include <string.h>
using namespace std;    

class String
{
private:
    char* m_Buffer;      //指向字符缓冲区
    unsigned int m_Size; //保存string的大小
public:
    String(const char* string) 
    {
        m_Size   = strlen(string);
        m_Buffer = new char[m_Size + 1];
        m_Buffer[m_Size] = 0;
        
        // 把这个指针复制到实际的缓冲区,这样缓冲区就会被我们的字符填充
        // 法一:
        // for (int i = 0; i < m_Size; i++)
        //     m_Buffer[i] = string[i];
        // 法二:更简洁
        memcpy(m_Buffer, string, m_Size);
    }

    String(const String& other) : m_Size(other.m_Size)
    {
        m_Buffer = new char[m_Size + 1];
        memcpy(m_Buffer, other.m_Buffer, m_Size + 1);
    }

    ~String()
    {
        delete[] m_Buffer;
    }

    char &operator[](unsigned int index)
    {
        return m_Buffer[index];
    }
    friend ostream &operator<<(ostream &stream, const String &string);
};

// 重载输出运算符,将 `String` 对象输出到流 `std::ostream` 中。
ostream& operator<<(ostream& stream, const String& string)
{
    stream << string.m_Buffer;
    return stream;
}


int main()
{
    String string = "Cherno";
    String second = string;
    second[2] = 'a';
    cout << string << endl;
    cout << second << endl;

    cin.get();
}

cherno建议:在基础使用中用 const 引用更好。 总是要用 const 引用传递对象,因为你可以在你写的的函数的内部决定是否要复制,但是你没有理由到处复制,会拖慢你的程序。 重要的事情说三遍,当你传递字符串的时候,不管这个字符串是你自己的 String 类还是标准库里的 String(std::string),总是要通过 const 引用来传递。

C++的vector使用优化
  • 观察以下代码:
代码语言:cpp
复制
struct vertex
{
    float x, y;

    vertex(float x, float y) : x(x), y(y) {}

    vertex(const vertex& other) : x(other.x), y(other.y) 
    {
        cout << "Copied!." << endl;
    }
};


int main()
{
    vector<vertex> vv;
    vv.push_back(vertex(1.0, 2.0));
    vv.push_back(vertex(3.0, 4.0));
}
// 此时进行了3次复制
/*输出:
Copied!.
Copied!.
Copied!.
*/

// 如果在增加一个元素:
vv.push_back(vertex(5.0, 6.0));
// 此时进行了6次复制
/*输出:
Copied!.
Copied!.
Copied!.
Copied!.
Copied!.
Copied!.
*/
  • 出现上面的原因解释:1、当我们创建 vertex 时,我们实际上是在主函数的current stack frame(当前栈帧)中构造它,所以我们是在 main 的栈上创建它。然后我们需要把这个刚创建的 vertex 从 main 函数里放到实际的 vector 中,放到 vector 分配的内存中。2、vector在这里调整了两次大小。
  • 优化策略:
  • 让vector一开始就留下足够的内存: vertices.reserve(3); 此时节省一般,只复制3次,如果不用reserve的话复制次数会指数倍地增长vertices.reserve(3); vertices.emplace_back(1, 2, 3); vertices.emplace_back(4, 5, 6); vertices.emplace_back(7, 8, 9); // 此时一个copies都没有了
  • 使用 emplace_back ,表示不是传递我们已经构建的 vertex 对象,而是只是传递了构造函数的参数列表

析构函数:与构造函数作用相反,用来释放一个对象

~类名标识符

  • 直接看下面代码:
代码语言:cpp
复制
class Qaq
{
public:
    float x, y;

    Qaq()
    {
        x = 1.0f;
        y = 2.0f;
    }

    ~Qaq()
    {
        cout << "Destroyed Qaq" << endl;
    };

    void Print()
    {
        cout << x << ", " << y << endl;
    }
};


int main()
{
Qaq q;
q.Print(); 
} 
//输出:1, 2
//      Destroyed Qaq

继承

class 类名 : 继承方式 继承方式有3种:public、protected、private

虚函数:在基类中使用virtual声明函数是虚函数后,就可以允许在子类中重写这个函数

  • 虚函数的使用:
代码语言:cpp
复制
class Paper
{
public:
    string GetName() { return "Paper"; }
};

class Cver : public Paper
{
private:
    string m_Name;
public:
    Cver(const  string& name) : m_Name(name) {}
    string GetName() { return m_Name; }
};
int main()
{
    Paper* w = new Paper();
    cout << w->GetName() << endl;

    Cver* p = new Cver("Cver");
    cout << p->GetName() << endl;

    Paper* qq = p;
    cout << qq->GetName() << endl;  
 
}
/* 输出:
Paper
Cver
Paper
*/



//声明虚函数后
class Paper
{
public:
    virtual string GetName() { return "Paper"; }
};

class Cver : public Paper
{
private:
    string m_Name;
public:
    Cver(const  string& name) : m_Name(name) {}
    string GetName() { return m_Name; }
};
int main()
{
    Paper* w = new Paper();
    cout << w->GetName() << endl;

    Cver* p = new Cver("Cver");
    cout << p->GetName() << endl;

    Paper* qq = p;
    cout << qq->GetName() << endl;  
 
}
int main()
{
    Paper* w = new Paper();
    cout << w->GetName() << endl;

    Cver* p = new Cver("Cver");
    cout << p->GetName() << endl;

    Paper* qq = p;
    cout << qq->GetName() << endl;  
 
}
/* 输出:
Paper
Cver
Cver
*/
纯虚函数:含有pure virual function的类称为抽象类。

virual 类型 函数名(参数列表)=0;

  • 纯函数必须被实现才能创建这个类的实例
代码语言:cpp
复制
// 比如我想要实现一个可以打印类名的功能

// 根据程序“由下而上”的思想,首先实现一个打印功能 Print()
void Print(Print_ClassName* obj)
{
    cout << obj -> GetClassName() << endl;
};

// Print_ClassName是需要定义的类,其中的获取类名的函数GetClassName()
// 就可以用pure virtual function实现,然后由子类再去实现这个函数

// 定义Print_ClassName,留出GetClassName这个接口
class Print_ClassName
{
public:
    virtual string GetClassName() = 0;
};

// 定义AA这个类,并且实现纯函数
class AA: public Print_ClassName
{
public:
    string GetClassName() override {return "AA"; }
};

int main()
{
    Print(new AA());
}
// 输出:AA

静态(static)

  • 正常定义一个结构体以及声明静态变量:
代码语言:cpp
复制
struct Entity
{
    int x, y;

    void Print()
    {
        cout << x << ", " << y << endl;
    }
};
int main()
{
 Entity e;
    e.x = 2;
    e.y = 3;

    Entity e1;
    e1.x = 5,
    e1.y = 7;
    e.Print();
    e1.Print();
}
//输出结果:
// 2, 3
// 5, 7


//声明静态成员
struct Entity
{
    static int x, y;

    void Print()
    {
        cout << x << ", " << y << endl;
    }
};

int Entity::x; //需要在类的外部对静态数据成员进行初始化
int Entity::y;
int main()
{
 Entity e;
    e.x = 2;
    e.y = 3;

    Entity e1;
    e1.x = 5,
    e1.y = 7;
    e.Print();
    e1.Print();
}
//输出结果:
//5, 7
//5, 7
  • 注意:静态方法不能访问静态变量

五、数据结构

栈(stack)和堆(heap)

  • 栈和堆是 RAM 中实际存在的两个区域: 栈通常是一个预定义大小的内存区域,通常约为 2MB左右; 堆也是一个预定义了默认值的区域,但是它可以增长,并随着应用程序的进行而改变。 这两个内存区域的实际物理位置都是在 RAM
  • 在堆上分配内存需要用new关键字
代码语言:cpp
复制
// 栈上定义int array
int value = 5;
int arary[5];

// 堆上定义int array
int* value1 = new int;
*value1 = 10;
int * array1 = new int[5];


class Entity
{
public:
    Entity()
    {
        cout << "created Entity!" << endl;
    }

    ~Entity()
    {
        cout << "Destroyed Entity!" << endl;
    }
};

int main() 
{
    Entity e;    //在栈上创建的   

	Entity* e = new Entity;   //在堆上创建的
}
  • 尽量在栈上分配内存,在堆上分配的唯一原因是如果你不能在栈上分配,比如你需要让它的声明周期比你在处理的作用域更长。

栈:栈上创建的变量超出作用域就会消失

  • 栈中分配内存时,一旦这个作用域结束,你在栈中分配的所有内存都会被弹出,内存被释放。这个作用域可以是任何形式,比如 main 函数,或者只是个空作用域,甚至可以使 for、while 循环等任何作用域语句。

堆:在堆中分配new后要调用delete关键字来释放内存

  • new关键字实际上调用了一个叫做malloc的函数的缩写,这样做通常会调用底层操作系统或平台的特定函数,这将在堆上为你分配内存。当你使用malloc请求堆内存时,它可以浏览空闲列表,找到一块符合大小要求的内存块,然后返回你一个它的指针,并记录分配的大小和它现在是否被分配的情况

六、其他小技巧

  • cpp中默认情况下函数只能返回一种类型,一个特定的变量,需要返回多个相同类型的变量时可以使用vector或者数组,但是多个不同类型时,可以采用以下几种方法: 1️⃣⭐使用struct(优先使用该方法,Cherno建议) 实现一个返回字符串索引和值的 函数:
代码语言:cpp
复制
#include <iostream>
#include <vector>

struct str_int
{
    int  index;
    char value;
};

std::vector<str_int> get_string_index(std::string &str){
    std::vector<str_int> result;     
    for(int i=0; i<str.size(); i++){
        str_int temp;       // temp 存储了一个int类型和char类型
        temp.index = i;
        temp.value = str[i];
        result.push_back(temp); 
    }
    return result;
};

int main(){
   std::string str = "hello";
   std::vector<str_int> result = get_string_index(str);
   for(auto i: result){
       std::cout << i.index << " " << i.value << std::endl;
   }
}
//输出:
/*
0 h
1 e
2 l
3 l
4 o
*/

2️⃣tuple和pair

● pair:使用.first 和.second 访问元素

代码语言:cpp
复制
#include <iostream>
#include <vector>

std::vector<std::pair<int, char>> get_string_index(std::string &str){
    std::vector<std::pair<int, char>> result;
    for(int i=0; i<str.size(); i++){
        std::pair<int, char> temp; // temp 存储了一个int类型和char类型
        temp.first = i;
        temp.second = str[i];
        result.push_back(temp); 
    }
    return result;
};

int main(){
   std::string str = "hello world";
   std::vector<std::pair<int, char>> result = get_string_index(str);
   for(auto i: result){
       std::cout << i.first << " " << i.second<< std::endl;
   }
}

● tuple:使用get<i>(name) 访问元素第i个元素

代码语言:cpp
复制
#include <iostream>
#include <vector>
#include <tuple>    

// struct str_int
// {
//     int  index;
//     char value;
// };



std::vector<std::tuple<int, char>> get_string_index(std::string &str){
    std::vector<std::tuple<int, char>> result;
    for(int i=0; i<str.size(); i++){
        std::tuple<int, char> temp; // temp 存储了一个int类型和char类型
        std::get<0>(temp) = i;
        std::get<1>(temp) = str[i];
        result.push_back(temp); 
    }
    return result;
};

int main(){
   std::string str = "hello";
   std::vector<std::tuple<int, char>> result = get_string_index(str);
   for(auto i: result){
       std::cout << std::get<0>(i) << " " << std::get<1>(i) << std::endl;
   }
}

3️⃣指针和引用

代码语言:cpp
复制
#include <iostream>

void returnWithReference(std::string& str, int& num)
{
    str = "Hello";
    num = 42;
}

int main()
{
    std::string str;
    int num;
    returnWithReference(str, num);
    std::cout << str << ", " << num << std::endl;
    return 0;
}
// 输出:
/*
Hello, 42
*/

模板

  • 模板允许你定义一个根据你的用途进行编译的模板,你可以让编译器基于一套规则帮你写代码。模版在调用函数时才开始根据所给的参数进行函数创建。(依赖于编译器,有些编译器只要不调用模板,其中的语法错误并不会影响编译)

template <typename 形参名, typename 形参名..> typename 可以使用class 替代

代码语言:cpp
复制
// 实现一个打印函数,由于输入类型不同,需要多次重载
#include <iostream>
#include <string>

void Print(int value)
{
    std::cout << value << std::endl;
}

void Print(std::string value) // 一次函数重载
{
    std::cout << value << std::endl;
}

int main()
{
    Print(5);
    Print("Hello");
    Print(4.3f); // 这里要是再想打印一个float类型还得再重载一次
    std::cin.get();
}

此时使用模版简化:

代码语言:cpp
复制
template<typename T>
void Print(T value)
{
    std::cout << value << std::endl;
}

也可以使用整数作为模版参数:

代码语言:cpp
复制
template <int N>
class Array
{
private:
    int m_Array[N];

public:
    int GetSize() const { return N; }
};

int main()
{
    Array<5> array;
    std::cout << array.GetSize() << std::endl; // 5
    std::cin.get();
}

写一个可以自动创建的类

代码语言:cpp
复制
template <typename T, int N>
class Array
{
private:
    T m_Array[N];

public:
    int GetSize() const { return N; }
};

int main(){
    // 调用
    Array<int, 5> array0;
    Array<std::string, 50> array1;
}

● 总结:

模板类型参数template<typename T> 此时可以像使用任何其他类型一样使用T。

非类型模板参数:允许我们在模板中使用常量值作为参数。用于在模板定义中指定一个常量值,而不是一个数据类型。非类型模板参数可以是整数、枚举、指针或引用类型。例如:template<int N>

○ 可以使用模板来做一些很好的事,比如日志系统记录每一种可能的类型,但是不要将模板写的复杂,因为后续就很难知道到底发生了什么。

使用#define定义,例如 #define PI 3.14159265

  • 实际上就是代码中的命令使用定义的名称替换,所以结尾的封号;也会一并替换
代码语言:cpp
复制
#define LOG(x) std::cout << x << std::endl;
int main(){
   LOG("Hello, World!")
}

// 或者

#define LOG(x) std::cout << x << std::endl
int main(){
   LOG("Hello, World!");
}
  • 一般使用宏不会使用上面的例子,因为你记录日志的方法可能基于你的设置会发生变化,可能希望 Debug 版本中有日志,而 Release 版本中去掉日志,而宏可以做到这一点:
  • 另外,宏可以分段写,通过\来表示换行,注意反斜杠后面不要有空格,不然就变成对空格转义了。
代码语言:cpp
复制
#define LOG(x) std::cout << x << std::endl;
#define MAIN int main() \
{\
    LOG("Hello, World!")\
}
MAIN

lambda表达式

capture list -> return type { function body } capture list 是捕获列表,用于指定 Lambda表达式可以访问的外部变量,以及是按值还是按引用的方式访问。捕获列表可以为空,表示不访问任何外部变量。 parameter list 是参数列表,用于表示 Lambda表达式的参数,可以为空,表示没有参数 return type 是返回值类型,用于指定 Lambda表达式的返回值类型,可以省略 function body 是函数体,用于表示 Lambda表达式的具体逻辑 =,传递所有变量,通过值传递;&传递所有变量,通过引用传递

  • 例子:std::find_if 函数在 values 容器中查找满足 lambda 表达式的条件的第一个元素。 现在使用lambda表达式找到第一个满足条件的元素:
代码语言:cpp
复制
#include <algorithm>
auto it = std::find_if(values.begin(), values.end(), [](int value) {return value > 3; });
std::cout << *it << std::endl;

# 相当于以下的功能:
void ForEach(const std::vector<int>& values,const std::function<void(int)>& func)
{
    for (int value : values)
        if(value > 3)    // 添加bool判断
            func(value);
}

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 一、学习资料📖
  • 二、基础知识
    • 编译:将cpp文件变成obj文件
    • 连接:将编译后的目标文件变成可执行程序
    • 关键字
      • const
      • mutalble:声明后可以在类的const函数里改变类的成员变量值
      • new:初始化变量,返回的是指向对象(或指向数组初始对象)的适当类型化的非零指针。在堆上分配内存
      • explicit:用来修饰只有一个参数的类构造函数,以表明该构造函数是显式的,而非隐式的,即禁止类对象之间的隐式转换,以及禁止隐式调用拷贝构造函数。
      • this:一个指向当前对象实例的指针
      • auto:让 C++自动推导出数据的类型
  • 三、数据类型
    • 数组:有序数据的集合
      • 静态数组(std::array)
    • 字符和字符串:字符使用''。字符串使用""
    • 指针:本质就是存储内存地址的整数
      • 函数指针:Function pointer是将一个函数赋值给一个变量的方法
    • 引用:相当于变量的一个别名,对它的操作和原来对象的操作相同
    • 枚举类型
  • 四、类和结构体
    • 可见性:public、private、protected
    • 重载运算符:运算符实际就是一个函数,所以实际上就是函数的重载
    • 箭头操作符
      • 构造函数:特殊的方法,在创建类的实例时运行,主要用来初始化该类
      • 析构函数:与构造函数作用相反,用来释放一个对象
      • 继承
      • 虚函数:在基类中使用virtual声明函数是虚函数后,就可以允许在子类中重写这个函数
    • 静态(static)
  • 五、数据结构
    • 栈(stack)和堆(heap)
      • 栈:栈上创建的变量超出作用域就会消失
      • 堆:在堆中分配new后要调用delete关键字来释放内存
  • 六、其他小技巧
    • 模板
    • lambda表达式
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档