C++基础笔记(写给有其他语言基础看的)

基础

暂停程序的执行

system("pause");

如果用CL可以不输出这个

#include <iostream>
using namespace std;

int main() {
    //count:标准输出流对象,通常指控制台,count定义在<iosteam>头文件中,全称为std::count
    // <<流插入运算符,将右侧的数据发送到左侧的输出流对象。<<其实是运算符,c++支持运算符重载,本质上是函数链式调用
    //endl 操纵器, 功能是插入一个换行符并刷新输出缓冲区,相当于std::flush
    // 整个表达式等价于一个函数调用:cout.operator<<(...)
    cout << "hello world" << endl;

    int a = 10;
    //这个函数返回本身,并且做了方法重载,因此可以接收各个不同的参数
    cout << "a = " << a << endl;
    return 0;//默认缺省 返回0,return 0没有异常,1就是程序报错,-1特殊报错,取决于决定
}

define宏常量 预处理阶段进行文本替换

cost的编译阶段

using namespace

这会将指定命名空间中的所有名字“拉”到当前作用域,之后使用这些名字时就不需要再写命名空间前缀了。

使用using声明更精确

#include <iostream>
using std::cout; // 只引入std命名空间中的cout
using std::endl; // 只引入std命名空间中的endl

int main() {
    cout << "Hello, world!" << endl; // 可以直接使用cout和endl
    // 但使用std命名空间的其他成员,如cin,仍然需要std::cin
    return 0;
}

虽然 using namespace很方便,但如果使用不当会带来问题,尤其是命名污染(namespace pollution)。请遵循以下最佳实践:

避免在头文件中使用 using namespace

这是最重要的原则。头文件通常会被多个源文件包含。如果在头文件里使用了 using namespace,那么这个命名空间就会被强制引入到所有包含了该头文件的源文件中,很容易引起难以排查的命名冲突

尽量在局部作用域内使用

即使在源文件(.cpp)中,也最好将 using namespaceusing声明限制在函数内部等尽可能小的作用域里,而不是放在文件开头全局使用,以减小其影响范围

  • 优先使用 using声明相比于引入整个命名空间的 using namespace指示,更推荐使用只引入特定成员的 using声明(如 using std::cout;)。这样做更安全,能最大程度地避免命名冲突

using类似于java的import static,using namespace是import *

数据类型

int的取值范围通常是2^32次方,short2字节,int 4字节,long windows 4字节,linux 32位4字节,64位 8字节

long long 16字节。具体按照平台定义和编译器实现。

int main() {
    cout << sizeof(short) << endl;//win输出2
    cout << sizeof(int) << endl;//win输出4
    cout << sizeof(long) << endl;//win输出4
    cout << sizeof(long long) << endl;//win输出8
    return 0;
}

浮点型:float是固定的,double由系统决定。

float 4字节 有效数字范围是7位 定义的时候要在数字结尾+f,如

1'000'000.0f

double 8字节 有效数字范围是7位

默认情况下输出小数,会显式6位有效数字。

字符类型

'c'

char在c++中只占用一个字节。

字符串:由一个一个字符组成的。

#include <iostream>
//需要引入才能使用
#include<string>
using namespace std;

int main() {
    //c风格的字符串
    char str[] = "hello world!";
    //c++风格的字符串
    string str2 = "asd";
    std::cout << str2 << endl;
    return 0;
}

数据输入

#include <iostream>

using namespace std;

int main() {
    int a = 0;
    cout << "请给整形变量a赋值" << endl;
    cin >> a;
    cout << a << endl;
}

数组

和java不同C++,C++数组不记录长度。越界的数组访问可能产生未定义的行为。c++直接定义数组是在栈上分配的。

int main() {
    int a[3] = {1, 2, 3};
    for (int i = 0; i < 3; i++) {
        cout << a[i] << endl;
    }
}

函数

#include <iostream>

using namespace std;

//如果函数写在下面的化要进行声明
void fancyfunction();

int main() {
    fancyfunction();
}

void fancyfunction() {
    cout << 1 << endl;
}

函数分页编写

我们需要创建.h后缀的头文件

自己的文件用引号,库里的文件用角括号

这是启动文件

#include <iostream>
#include "head.h"
using namespace std;

//如果函数写在下面的化要进行声明


int main() {
    fancyfunction();
}

这是head.h文件头文件一般约定俗成用来放函数声明

void fancyfunction();

这是被引入的文件,需要在cmake里定义

#include <iostream>
using namespace std;


void fancyfunction() {
    std::cout << 1 << std::endl;
}

cmake

cmake_minimum_required(VERSION 3.28)
project(c__17)

set(CMAKE_CXX_STANDARD 17)

add_executable(c__17 main.cpp
        Main.cpp
        tool.cpp
        head.h
)

cmake用来编译的时候链接文件。

cmake_minimum_required(VERSION 3.28)

  • 作用:指定构建本项目所需的 CMake 最低版本 为 3.28

project(c__17)

  • 作用:定义项目的名称(这里为 c__17

add_executable(c__17 main.cpp Main.cpp tool.cpp head.h)

  • 作用:指示 CMake 从后面列出的源文件(main.cpp, Main.cpp, tool.cpp)生成一个名为 c__17的可执行文件

文件

include是预处理阶段的文本替换。

#include <iostream>等效于把iostream里的代码全部复制过来

using: 将特定命名空间中的名称引入当前作用域,使其无需限定即可使用

using namespace std;告诉编译器,

当遇到一个没有明确指定命名空间的标识符时,如果std命名空间中有同名的标识符,就使用std中的那个。

using namespace是C++中的命名空间指令,主要功能是将指定命名空间中的所有标识符引入当前作用域,让我们可以直接使用这些标识符而无需添加命名空间前缀。

指针

*是指针变量名

指针前加*代表解引用。

int main() {
    int a = 10;
    int *p;
    p = &a;
    cout << "a地址是" << sizeof(p) << "值是" << *p;
}

a地址是8值是10(32位是4,64是8)

#include <iostream>
using namespace std;

int main() {
    int *p = (int *) 0x1100;
    //访问野指针,只想非法的空间

    int a = 10;
    int b = 10;
    //const修饰指针,指针指向可以修改,但指向的值不能修改
    const int *p2 = &a;
    //*p2=20;//错误代码
    p = &b; //正确


    //指针常量 指针的指向不可以改,指针指向的值可以改
    int *const p3 = &a;

    //都不可以改
    const int *const p4 = &a;
    cout << "地址是" << sizeof(p) << "值是" << *p;
}

函数指针

int main() {
    int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9};

    int *p = arr; //arr就是数组首地址

    //输出数组的第一个
    cout << *p << endl;
    *p = 10;
    cout << arr[0]<< endl;
    //用指针访问第二个元素
    p += 10; //如果+10可能会访问到未知元素

    cout << *p << endl;
}

c++中数组默认是指针传递(其实这是因为数组的本质就是指针,因此实际上还是值传递)

#include <iostream>
using namespace std;

void func(int arr[]) {  // 等价于 int* arr
    cout << "sizeof(arr) in function: " << sizeof(arr) << endl;  // 输出指针大小
}

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    cout << "sizeof(arr) in main: " << sizeof(arr) << endl;  // 输出数组总大小
    
    func(arr);  // 实际传递的是 &arr[0]
    return 0;
}

引用传递

#include <iostream>
using namespace std;

void swap(int &a, int &b) noexcept {
    const int temp = a;
    a = b;
    b = temp;
}

int main() {
    int a = 1;
    int b = 2;
    swap(a, b);
    cout << a << endl;
    cout << b << endl;
}

结构体

结构体属于用户自定义的数据类型

结构体也是值传递,结构体可以当做所有成员都是public的类。

#include <iostream>
using namespace std;

//结构体继承
struct student  {
    string name;
    int age;

    //结构体里可以定义结构体
    struct teacher {
        string name;
        int age;
    };

    int getAge() {
        //这里的this是只想自己的指针
        return this->age;
    }
} x2;

//在创建结构体的时候,自动初始化变量,最好不要这样写

int main() {
    struct student st;
    st.name = "123";
    cout << st.name << endl;
    //按顺序即可
    struct student s2 = {"eee", 123};
    //struct关键字可以省略,c20可以不按顺序写,只要声明了名字
    student s3 = {.name = "eee", .age = 123};
    //数组
    student s4[10] = {{"", 123}, {"qaq", 333}};

    //指针
    student *p = &st;
    st.age = 10; //如果不给值随机输出,可能访问到奇怪的东西
    std::cout << p->age << endl;
    //结构体里的方法
    std::cout << st.getAge() << endl;
}

继承

#include <iostream>
using namespace std;


struct person {
    //可以有默认值
    int age = 1;
};

//go语言默认是public继承
struct student : public person {
    string NO = "qweqwc";
};

//在创建结构体的时候,自动初始化变量,最好不要这样写

int main() {
    student s = {1, ""};
    cout << s.age << endl;
}

程序地址

全局区

//全局变量
int a = 1;
//函数名不能和变量名一样
void ca() {
    //普通变量前面加上static就是静态变量
    //只创建一次 static也是全局的
    static int a = 10;
    //第二次输出11
    cout << a << endl;
    a = 11;
}

int main() {
    ca();
    ca();
}

struct person {
    int age = 1;
};
int main() {
    person *p = new person();
    cout << p->age << endl;
}

new

#include <iostream>
using namespace std;

struct person {
    int age = 1;
};

int main() {
    person *p = new person();
    cout << p->age << endl;
    //释放堆内存
    delete p;
}

函数

#include <iostream>
using namespace std;

int func(int a, int b = 20, int c = 30) {
    return a + b + c;
}

int func(int a) {
    return a;
}

//函数站位
int func(string) {
    return 1;
}

int main() {
    cout << func(20, 10) << endl;
}

类对象

class和struct的唯一区别就在于默认的访问权限不同。

析构函数不可以有参数,不能重载,在销毁前自动调用析构函数。

#include <iostream>
using namespace std;

class persion {
public:
    int age = 10;

    persion() {
        cout << "我是人" << endl;
        this->age = 10;
    }

    persion(int age) {
        cout << "我是人" << endl;
        this->age = age;
    }

    void q() {
        cout << "hello world" << endl;
    }

    ~persion() {
        cout << "我被调用了" << endl;
    }
};

class student {
public:
    int age;
};

int main() {
    //声明的时候自动调用构造函数
    persion per;
    cout << per.age << endl;
    //new是把内存分配在堆上,和java有所不同 如果没有构造函数也可以这样直接赋值
    persion p2{3};

    persion *p = new persion();
    p->q();
    //delete自动调用析构函数
    delete p;

    student stu; //直接访问到了内存中混乱的值
    cout <<"学生的默认年龄" << stu.age << endl;
}

构造函数

#include <iostream>
using namespace std;


class student {
public:
    int age;

    student(int age) {
        this->age = age;
    }

    //拷贝构造,剩下的构造都是普通构造。传递参数的时候会调用拷贝构造函数
    student(const student &p) {
        cout << "拷贝构造被调用了" << endl;
        age = p.age;
    }
};

void func(student s) {
    cout << s.age << endl;
}

int main() {
    //因为手动定义了构造函数因此就要传参了
    student s(1);
    cout << s.age << endl;
    //这样也可以初始化
    student s2{1};
    cout << s2.age << endl;
    //值传递自动调用拷贝构造
    func(s2);
}

浅拷贝,编译器默认生成的拷贝构造函数和赋值运算符重载函数就是浅拷贝。

浅拷贝的问题总结(双重危机)

  1. 悬空指针:多个对象共享同一块动态内存,其中一个对象被销毁并释放内存后,其他对象的指针就变成了悬空指针,使用它们会导致未定义行为。
  2. 重复释放:当所有共享该内存的对象都销毁时,每个对象的析构函数都会尝试释放同一块内存,这会导致程序崩溃。

深拷贝

深拷贝解决了浅拷贝的所有问题。它的核心思想是:连“葫芦”带“瓢”一起复制。即,不仅复制指针本身,还为新的对象重新分配一块内存,并将原指针所指内存的内容完整地复制过来。

#include <iostream>
using namespace std;


class student {
public:
    int age;
    int *height;

    student(int age) {
        this->age = age;
        this->height = new int(1);
    }

    student(int age, int *height) {
        this->age = age;
        this->height = height;
    }

    student(const student &p) {
        cout << "拷贝构造被调用了" << endl;
        age = p.age;
        height = p.height;
    }

    //如果删掉 那么就是内存泄露了
    ~student() {
        if (height != NULL) {
            delete height;
        }
    }
};


int main() {
    int a = 2;
    student *s1 = new student(1, &a);
    //不能释放栈上的东西
    delete s1;
}
int main() {
    student *s1 = new student(1);
    student *s2 = new student(*s1);
    delete s1;
    //内存被释放了俩次,会报错
    delete s2;
}

类中的函数一般被称为成员函数。静态成员就是在成员变量和成员函数之前加上static关键字,就称为静态成员。

静态成员分为静态成员变量和静态成员函数。类一般放在hpp文件里,因为许多类要被用到多次。

#include <iostream>
using namespace std;

class student {
public:
    //这里不能初始化
    static int a;


    static int pt() {
        cout << "hello world" << endl;
        return 0;
    }
    //这里要按顺序写
    //c++17开始允许在内中使用inline声明+定义
    inline static int b = student::pt();
};

//要在类外进行定义
int student::a = 1;


int main() {
    cout << student::a << endl;
    student::pt();
    cout << student::b << endl;
}

c++指针默认指向调用发起者,可以在调用的时候判断this是否会空。

#include <iostream>
using namespace std;

class student {
public:
    //这里不能初始化
    static int a;


    static int pt() {
        cout << "hello world" << endl;
        return 0;
    }

    //这里要按顺序写
    //c++17开始允许在内中使用inline声明+定义
    inline static int b = student::pt();

    int bc = 0;

    void func() {
        //为了防止空指针调用这个函数报错,可以先进行判断
        if (this != nullptr) {
            //和java相似直接写bc等于this->bc
            cout << this->bc << endl;
        } else {
            cout << "你调用了一个空方法" << endl;
        }
    }
};

//要在类外进行定义
int student::a = 1;


int main() {
    cout << student::a << endl;
    student::pt();
    cout << student::b << endl;
    auto s = new student();
    //最好不要直接用NULL因为NULL是宏定义的0值
    s = nullptr;
    //劲量在调用之前写
    s->func();
}

final和const

c++中final用于继承控制

// 禁止类被继承
class Base final {
    // ...
};
// class Derived : public Base {}; // 错误:Base 是 final 的

// 禁止虚函数被重写
class Animal {
public:
    virtual void speak() final { // 此函数不能被子类重写
        cout << "Animal sound" << endl;
    }
};

class Dog : public Animal {
public:
    // void speak() override { } // 错误:speak 是 final 的
};

const用于定义常量,表示"不可修改"。

  • 常量变量
  • 常量成员函数
  • 常量指针和引用
  • 函数参数和返回值的常量性
// 常量变量
const int MAX_SIZE = 100;
// MAX_SIZE = 200; // 错误:常量不可修改

// 常量成员函数
class MyClass {
    int value;
public:
    int getValue() const { // 承诺不修改对象状态
        return value;
    }
};

// 常量指针
int x = 10;
const int* ptr = &x; // 指向常量的指针
// *ptr = 20; // 错误:不能通过ptr修改x

int* const ptr2 = &x; // 常量指针(指针本身不可修改)

友元

让特定的函数可以访问private的字段。同样的可以让特定类里的特定函数访问private字段。

#include <iostream>
using namespace std;

class student {
private:
    string primarySchool = "100";

public:
    friend void look(student &s);
};

void look(student &s) {
    cout << s.primarySchool << endl;
}

int main() {
    student s;
    look(s);
}

类友元

class MyClass {
private:
    int secret_data;

    // 声明 FriendClass 是 MyClass 的友元
    friend class FriendClass;

public:
    MyClass(int data) : secret_data(data) {}
};

class FriendClass {
public:
    void showSecret(MyClass& obj) {
        // FriendClass 可以直接访问 MyClass 的私有成员 secret_data
        std::cout << "The secret data is: " << obj.secret_data << std::endl;
    }

    void modifySecret(MyClass& obj, int new_value) {
        // 也可以修改私有成员
        obj.secret_data = new_value;
    }
};

// 一个非友元的类
class StrangerClass {
public:
    void cannotShowSecret(MyClass& obj) {
        // 编译错误!secret_data 是 MyClass 的私有成员,无法访问。
        // std::cout << obj.secret_data << std::endl;
    }
};

int main() {
    MyClass my_obj(42);
    FriendClass friend_obj;
    StrangerClass stranger_obj;

    friend_obj.showSecret(my_obj); // 正确输出:The secret data is: 42
    friend_obj.modifySecret(my_obj, 100);
    friend_obj.showSecret(my_obj); // 正确输出:The secret data is: 100

    // stranger_obj.cannotShowSecret(my_obj); // 这行代码会导致编译错误

    return 0;
}

运算符重载

c++,c#,rust,python都支持,php,java,js,go等不支持

#include <iostream>
using namespace std;

class person {
public:
    int a = 10;
    int b = 10;

    person() {
        cout << "对象创建" << endl;
    }

    person operator+(person &p) {
        person temp;
        temp.a = this->a + p.a;
        temp.b = this->b + p.b;
        return temp;
    }
};

person operator-(person &p1, person &p2) {
    person temp;
    temp.a = p1.a - p2.a;
    temp.b = p1.b - p2.b;
    return temp;
}

void t1() {
    person p1;
    person p2;
    //p3在这里也会创建一次
    person p3 = p1 + p2;
    cout << p3.a << endl;
    cout << p3.b << endl;
}


int main() {
    //都是创建了三次对象
    person p1;
    person p2;
    person p4 = p1 - p2;
    cout << p4.a << endl;
    cout << p4.b << endl;
}

继承

// 默认是私有继承 (private),通常我们显式指定为 public
class Derived : public Base { // 公有继承
    // ...
};

class Derived2 : Base { // 私有继承(默认,不推荐这样写)
    // ...
};

虚方法

#include <iostream>
using namespace std;

class person {
public:
    //纯虚函数,子类必须要重写这个方法
    //virtual void func() =0;

    virtual void print() {
        cout << "hello world" << endl;
    }

    void print2() {
        cout << "hello world !" << endl;
    }
};

class man : public person {
public:
    void print() {
        person::print();
        cout << "hi world" << endl;
        //调用其他方法
        person::print2();
    }
};

int main() {
    man m;
    m.print();

    //这里会默认调用a父方法,除非将调用的方法声明virtual
    person *a = new man();
    a->print();
}

class不声明的方法默认是private的

#include <iostream>
using namespace std;

class A {
public:
    void func() {
        cout << "A:hello world" << endl;
    }
};

class B {
public:
    void func() {
        cout << "B:hello world!" << endl;
    }

    void func2() {
        cout << "B:hi world!" << endl;
    }
};

class C : public A, public B {
public:
    //覆盖&重写
    void func() {
        cout << "C:hello world" << endl;
    }
};


int main() {
    C c;
    //直接调用func2没问题
    c.func2();
    //重写可以解决
    c.func();
    //直接调用具体的方法
    c.B::func();
}

C++中overwrite代表重写和覆盖 ,是c++11引入的

class Base {
public:
    virtual void show() {
        cout << "Base class" << endl;
    }
    
    virtual void display(int x) {
        cout << "Base display: " << x << endl;
    }
};

class Derived : public Base {
public:
    // 正确重写基类的虚函数
    void show() override {
        cout << "Derived class" << endl;
    }
    
    // 使用override关键字,如果签名不匹配会报错
    void display(int x) override {
        cout << "Derived display: " << x << endl;
    }
};

overwrite 通常指函数隐藏,当派生类定义了与基类同名的函数(但不是虚函数重写)时发生。类似java的overwrite注解

class Base {
public:
    void normalFunc() {
        cout << "Base normal function" << endl;
    }
    
    void func(int x) {
        cout << "Base func: " << x << endl;
    }
};

class Derived : public Base {
public:
    // 这隐藏了基类的normalFunc(),不是重写
    void normalFunc() {
        cout << "Derived normal function" << endl;
    }
    
    // 隐藏了基类的func(int),即使参数不同
    void func(double x) {
        cout << "Derived func: " << x << endl;
    }
};

int main() {
    Derived d;
    d.normalFunc();        // 调用Derived版本
    d.func(5.0);           // 调用Derived版本
    // d.func(5);          // 错误!基类的func(int)被隐藏
    d.Base::func(5);       // 正确,显式调用基类版本
}

虚方法使用非常灵活,动态分派(Dynamic Dispatch)

抽象类

可以将虚函数写为纯虚函数。类中有了纯虚函数这个类也称为抽象类。

内联

class MyClass {
public:
    // 1. 在类内部直接实现 - 默认是inline的
    void func1() { 
        // 函数体直接在类定义中
        // 编译器会尝试将其作为inline函数处理
    }
    
    // 2. 只是声明,实现在类外部 - 默认不是inline的
    void func2();
};

// 在类外部实现 - 默认不是inline的
void MyClass::func2() {
    // 需要显式添加inline关键字才会成为inline函数
}

// 如果想要func2也是inline的,需要显式指定
inline void MyClass::func2() {
    // 现在这个函数是inline的
}

inline只是一个建议,编译器有权忽略复杂的函数

模板

函数模板

c++提供俩种模板机制:函数模板和类模板 c11引入的

建立一个通用函数,用一个虚拟的类型来代表。

#include <iostream>

//当写上template的时候,函数必须要用到这个T,或者调用声明的时候要用到这个T
template<typename T>
void swap(T &a, T &b) {
    T temp = a;
    a = b;
    b = temp;
}


int main() {
    //
    int a = 10;
    int b = 20;
    //编译器自动推导
    //再编译时自动生成一个调用int的函数,然后调用哪个函数
    swap(a, b);
    std::cout << a << std::endl;
    std::cout << b << std::endl;
    swap<int>(a, b);
}

使用多个泛型

template<typename T, typename U>
void printPair(T first, U second) {
    std::cout << "First: " << first << ", Second: " << second << std::endl;
}

// 使用示例
printPair(10, "Hello");        // T=int, U=const char*
printPair(3.14, true);         // T=double, U=bool

普通函数调用时可以发生自动类型转换。函数模板调用时,如果利用自动类型推导,不会发生隐式类型转换。

如果利用显式指定类型的方式,可以发生隐式类型转换。

普通函数和函数模板可以发生重载。但优先调用普通函数,函数模板页可以发生重载。

数组不能直接赋值

int arr1[5] = {1, 2, 3, 4, 5};
int arr2[5] = {6, 7, 8, 9, 10};

// 错误的!不能直接赋值
arr1 = arr2;  // 编译错误!

// 错误的!数组名不能作为左值
arr1 = {10, 20, 30};  // 编译错误!

因此函数模板处理数组可能出现错误。或者俩个对象可能是不能直接进行比较的。

#include <iostream>

class A {
public:
    int a = 2;
};

template<typename T>
T add(T &a, T &b) {
    return a + b;
}

//这样就会走这段代码
template<>
A add(A &a1, A &a2) {
    A temp;
    temp.a = a1.a + a2.a;
    return temp;
}


int main() {
    A a;
    A b;
    std::cout << add(a, b).a << std::endl;
}

类模板

#include <iostream>
#include <string>

//class和typename一样
template<class Name, class Age>
class persion {
public:
    persion(Name n, Age a) {
        this->name = n;
        this->age = a;
    }

    Name name;
    Age age;
};

int main() {
    //类模板要显式指定类型
    persion<std::string, int> p1("hh", 1);
    std::cout << p1.age << std::endl;
    std::cout << p1.name << std::endl;
}

类模板没有自动类型推导,类模板在模板参数列表中可以有默认参数。

类模板的成员函数创建实际是有区别的

  • 普通类中的成员函数一开始就可以创建
  • 类模板中的成员函数在调用时才创建

类模板对象可以作为函数参数

template<typename T>
class MyArray {
private:
    T m_array[100];
    int m_size;
public:
    MyArray() : m_size(0) {}
    void push_back(T value) {
        m_array[m_size++] = value;
    }
    int getSize() const {
        return m_size;
    }
    // ... 其他成员函数,如获取元素等
};
// 函数参数是 MyArray<int>,非常具体
void printArray(MyArray<int>& arr) {
    for (int i = 0; i < arr.getSize(); ++i) {
        // 假设有获取元素的成员函数
        // std::cout << arr.getElementAt(i) << " ";
    }
    std::cout << std::endl;
}

int main() {
    MyArray<int> intArray;
    intArray.push_back(10);
    intArray.push_back(20);

    MyArray<std::string> strArray; // 另一个类型的 MyArray 对象

    printArray(intArray); // 正确!类型完全匹配
    // printArray(strArray); // 错误!函数需要 MyArray<int>,但传入的是 MyArray<std::string>

    return 0;
}

子类继承父类 是一个类模板是,子类在声明的时候,要指定父类中T的类型 ,如果不指定,编译器无法给子类分配内存。如果想灵活指定父类中的T类型,子类变为类模板。

STL

长久以来软件界一直希望建立可重复利用的东西。大多数情况下,数据结构算法都未能有一套标准,导致大量的重复工作。为了建立数据结构和算法的一套标准诞生了STL。

STL广义上分为:容器Container,算法algorithm,迭代器iterator

STL分为六大组件,分别是:容器,算法,迭代器,仿函数,适配器,空间配置器。

容器

序列式容器和关联式容器:

序列是容器:强调值的排序,序列是容器中每个元素均有固定的位置。

关联式:二叉树结构,各个元素之间没有严格意义上的物理上的顺序关系

vector

#include <iostream>
#include <string>
#include <vector>
#include<algorithm>
using namespace std;

void my_print(int a) {
    cout << a << endl;
}

int main() {
    vector<int> v;
    v.push_back(10);
    v.push_back(20);
    v.push_back(30);

    //迭代器
    vector<int>::iterator itBegin = v.begin(); // 起始迭代器,指向第一个元素
    vector<int>::iterator itEnd = v.end(); // 结束迭代器,指向容器中最后一个元素的下一个位置

    //遍历
    while (itBegin != itEnd) {
        cout << *itBegin << endl;
        itBegin++;
    }
    for (vector<int>::iterator it = v.begin(); it != v.end(); it++) {
        cout << *it << endl;
    }
    //函数回调
    for_each(v.begin(), v.end(), my_print);

    //只读访问
    for (const auto &element: v) {
        cout << element << endl;
    }
    //需要修改元素
    for (auto &element: v) {
        element *= 2;
    }
    //使用下标索引,vector重载了下标
    for (int i = 0; i < v.size(); i++) {
        std::cout << v[i] << endl;
    }
}

c++默认数组是随机初始值(上次未清理的值)

int main() {
    int arr[10];
    //随机初始
    for (int i = 0; i < 10; i++) {
        cout << arr[i] << endl;
    }
    //c11开始变成固定初始值
    int arr1[10]{};
    for (int i = 0; i < 10; i++) {
        cout << arr1[i] << endl;
    }
}

构造函数和初始化

#include <vector>
using namespace std;

// 默认构造
vector<int> v1;

// 指定大小和初始值
vector<int> v2(5, 10); // 5个元素,每个都是10

// 从数组初始化
int arr[] = {1, 2, 3, 4, 5};
vector<int> v3(arr, arr + 5);

// 初始化列表(C++11)
vector<int> v4 = {1, 2, 3, 4, 5};

// 拷贝构造
vector<int> v5(v4);

元素访问

vector<int> v = {10, 20, 30, 40, 50};

// 下标访问(不检查边界)
cout << v[2]; // 30

// at() 方法(检查边界,越界抛出异常)
cout << v.at(2); // 30

// 访问第一个和最后一个元素
cout << v.front(); // 10
cout << v.back();  // 50

// 数据指针(C++11)
int* data = v.data();

数组容量

vector<int> v = {1, 2, 3};

cout << v.size();     // 当前元素个数:3
cout << v.capacity(); // 当前容量:可能大于size
cout << v.empty();    // 是否为空:false

v.reserve(100);       // 预分配空间,避免多次重新分配
v.shrink_to_fit();    // 减少容量到刚好容纳元素

在GCC和Clang中默认是0,扩容增量是2倍。也就是说·1,2,4,8,16·

插入删除

vector<int> v = {1, 2, 3};

// 末尾添加
v.push_back(4);       // v: {1, 2, 3, 4}

// 任意位置插入
v.insert(v.begin() + 1, 99); // v: {1, 99, 2, 3, 4}

// 插入多个相同元素
v.insert(v.end(), 3, 100); // 末尾插入3个100

// C++11 原地构造
v.emplace_back(5);    // 比push_back更高效
v.emplace(v.begin(), 0);

vector<int> v = {1, 2, 3, 4, 5, 6};

v.pop_back();         // 删除末尾元素
v.erase(v.begin() + 1); // 删除第二个元素
v.erase(v.begin(), v.begin() + 2); // 删除前两个元素
v.clear();            // 清空所有元素

string

在大部分

在绝大多数现代C++标准库实现中,std::string的底层确实使用了一个动态分配的字符数组(即 char*)来存储字符串数据。

string的本质上是一个类。是一个char *的容器。

#include <iostream>
#include <string>
#include <vector>
#include<algorithm>
using namespace std;


int main() {
    //默认构造
    string s1;
    cout << s1 << endl;
    const char *str = "hello world";
    cout << str << endl;
    string s3(str);
    cout << s3 << endl;
    string s4(10, '1');
    cout << s4 << endl;
    string a = "123";
    cout << a << endl;
}

基本操作

string s = "Hello";

// 获取长度
int len = s.length();           // 或 s.size()

// 检查是否为空
bool isEmpty = s.empty();

// 访问字符
char c1 = s[0];                 // 'H'
char c2 = s.at(1);              // 'e',带边界检查

// 修改字符
s[0] = 'h';                     // "hello"

字符串拼接

string s1 = "Hello", s2 = "World";

// + 运算符
string s3 = s1 + " " + s2;     // "Hello World"

// append() 方法
s1.append(" C++");              // "Hello C++"

// += 运算符
s1 += "!";                      // "Hello C++!"

字符串比较

string a = "apple", b = "banana";

if (a == b) cout << "相等";
else if (a < b) cout << "a小于b";  // 字典序比较
else cout << "a大于b";

// compare() 方法
int result = a.compare(b);      // 返回负数、0或正数

deque

vector对于头部的插入删除效率低,数据量越大效率越低

deque相对而言,对头部的插入删除速度比vector快。

deque内部有个中空气维护每段缓冲区中的内容。

deque在查找上相比于vector需要额外的地址计算,而且局部性较差。

#include <iostream>
#include <deque>

int main() {
    std::deque<int> dq;
    
    // 尾部插入
    dq.push_back(1);
    dq.push_back(2);
    
    // 头部插入
    dq.push_front(0);
    dq.push_front(-1);
    
    // 遍历输出:-1 0 1 2
    for (int num : dq) {
        std::cout << num << " ";
    }
    std::cout << std::endl;
    
    // 随机访问
    std::cout << "第二个元素: " << dq[1] << std::endl;
    
    // 头部删除
    dq.pop_front();
    
    // 尾部删除
    dq.pop_back();
    
    return 0;
}

stack

#include <iostream>
#include <stack>
using namespace std;

int main() {
    stack<int> s;
    
    // 压入元素
    s.push(1);
    s.push(2);
    s.push(3);
    
    cout << "栈顶元素: " << s.top() << endl;  // 输出3
    cout << "栈大小: " << s.size() << endl;   // 输出3
    
    // 遍历栈(遍历会清空栈)
    cout << "栈内容: ";
    while (!s.empty()) {
        cout << s.top() << " ";
        s.pop();  // 移除栈顶元素
    }
    // 输出: 3 2 1
    
    return 0;
}

Queue

先进先出c++stack和Queue底层大部分是deque

list

list底层是链表

#include <iostream>
#include <list>
#include <string>

// 自定义对象列表
struct Person {
    string name;
    int age;
    
    Person(string n, int a) : name(n), age(a) {}
};

int main() {
    list<Person> people;
    
    // 添加元素
    people.emplace_back("Alice", 25);
    people.emplace_back("Bob", 30);
    people.emplace_front("Charlie", 20);
    
    // 遍历
    for(const auto& p : people) {
        cout << p.name << ": " << p.age << endl;
    }
    
    // 自定义排序
    people.sort([](const Person& a, const Person& b) {
        return a.age < b.age;
    });
    
    return 0;
}

set、multiset

所有的元素在插入时会被自动排序。

set不允许有重复元素。multiset允许有重复元素。

创建

#include <set>
#include <iostream>

using namespace std;

int main() {
    // 声明一个空的 set
    set<int> s1;
    
    // 使用初始化列表
    set<int> s2 = {1, 3, 5, 2, 4};
    
    // 使用迭代器范围初始化
    vector<int> vec = {1, 2, 3, 2, 1};
    set<int> s3(vec.begin(), vec.end()); // 结果为 {1, 2, 3}
    
    return 0;
}

插入

set<int> s;

// 插入单个元素
s.insert(10);
s.insert(20);
s.insert(30);

// 插入多个元素(C++11)
s.insert({5, 15, 25});

// 使用 emplace(C++11),更高效
s.emplace(40);

// 检查插入是否成功
auto result = s.insert(20); // 20已存在,插入失败
if (!result.second) {
    cout << "插入失败,元素已存在" << endl;
}

删除操作

set<int> s = {1, 2, 3, 4, 5, 6};

// 通过值删除
s.erase(3); // 删除元素3

// 通过迭代器删除
auto it = s.find(4);
if (it != s.end()) {
    s.erase(it);
}

// 删除一个范围
auto it1 = s.find(2);
auto it2 = s.find(6);
if (it1 != s.end() && it2 != s.end()) {
    s.erase(it1, it2); // 删除 [2, 6) 范围的元素
}

// 删除所有元素
s.clear();

查找

set<int> s = {10, 20, 30, 40, 50};

// 使用 find
auto it = s.find(30);
if (it != s.end()) {
    cout << "找到元素: " << *it << endl;
}

// 使用 count(对于 set,只能是 0 或 1)
if (s.count(25) > 0) {
    cout << "元素存在" << endl;
}

// 检查是否存在某个范围的元素
if (s.lower_bound(20) != s.upper_bound(40)) {
    cout << "存在 20-40 之间的元素" << endl;
}

Map

int main() {
    pair<string,int>p("tom",20);
    cout<<p.first<<endl;
    cout<<p.second<<endl;
}

map中所有元素都是pair,第一个元素是key,第二个元素是value。key起到了所有作用。

#include <iostream>
#include <string>
#include <map>

using namespace std;


int main() {
    map<int, int> m;
    m.insert(pair<int, int>(1, 10));
    m.insert(pair<int, int>(2, 20));
    m.insert(pair<int, int>(3, 30));
    m.insert(pair<int, int>(4, 40));
    for (const auto &pair: m) {
        std::cout << "Key: " << pair.first << ", Value: " << pair.second << std::endl;
    };

    std::map<int, std::string> myMap = {
        {1, "Apple"},
        {2, "Banana"},
        {3, "Cherry"}
    };

}
#include <iostream>
#include <map>
int main() {
    std::map<int, std::string> mp;
    mp[1] = "one";
    mp.insert({2, "two"});
    mp.emplace(3, "three");

    for (const auto& pair : mp) {
        std::cout << pair.first << ":" << pair.second << " ";
    }
    // 输出:1:one 2:two 3:three
    return 0;
}

仿函数可以向函数一样被调用对象。通过重载函数调用运算符operator实现。(因为模板参数不能写函数指针,但是可以写类型)

这个map是红黑树。unorderedmap才是hashmap

Last modification:November 21, 2025
如果觉得我的文章对你有用,请随意赞赏