0%

C和Cpp语言语法注意事项小结

STL相关

  • 注意vector<>.size()返回的是无符号整数,unsigned同样大小类型的有符号整数相加的时候会将这个有符号整数类型转换为无符号的,导致表示的意思出现区别

  • 对无符号数字给负值的时候,实际的到的值是给的数字+该类型能表达的最大数字

    initializer_list<>

  • 用于不定参数个数的函数的传参

  • 只能传递同一个类型的参数

  • 复制的时候是浅拷贝

    algorithm

    函数|功能
    |—|—|
    find(begin, end, val)|在begin和end(不包括)之间找val,找不到返回end,找到返回位置迭代器,是线性查找
    stable_sort(begin, end, compare)|是稳定排序算法,会保证相等元素的顺序

  • 其他

算法 描述
std::sort 对给定范围的元素进行排序。
std::reverse 反转给定范围的元素。
std::rotate 将给定范围的元素循环右移指定位置。
std::find 在给定范围内查找指定值,返回第一个匹配元素的迭代器。
std::binary_search 在有序范围内执行二分查找,返回是否找到指定值的布尔值。
std::count 计算给定范围内等于指定值的元素个数。
std::accumulate 对给定范围内的元素进行累积(求和)操作。
std::max_element 返回给定范围内的最大元素的迭代器。
std::min_element 返回给定范围内的最小元素的迭代器。
std::copy 将一个范围的元素复制到另一个范围。
std::fill 将给定范围的元素都设置为指定的值。
std::unique 移除给定范围内的重复元素,返回指向新范围结尾的迭代器。
std::next_permutation 将给定范围的元素重新排列为下一个字典序排列。
std::prev_permutation 将给定范围的元素重新排列为前一个字典序排列。
std::shuffle 将给定范围内的元素随机重排。
std::transform 对给定范围内的元素执行指定操作,结果存储在另一个范围。
std::merge 将两个有序范围合并成一个有序范围。
std::partition 根据指定条件对给定范围进行分区,将满足条件的元素放在前面。
std::nth_element 对给定范围的元素进行局部排序,使得第n个元素是排好序的。

bind函数

  • 用于包装函数改变其参数列表
    auto newCallable = bind (callable, arg_list);
  • 将可调用对象和参数绑定成一个新的可调用对象,可以延迟调用或者传递给其他函数使用。
  • 将多元(参数个数大于1)的可调用对象转换成一元或者少元的可调用对象,即只绑定部分参数,剩下的参数在调用时传入。
    struct A
    {
    void print(int x, int y)
    {
    cout << x << " " << y << endl;
    }
    };
    // 将print函数和一个参数绑定成一个新的可调用对象,另一个参数用占位符表示
    auto f2 = bind(print, placeholders::_1, 2);
    f2(1); // 输出 1 2
    f2(3); // 输出 3 2
  • 注意bind后的函数传参是按照placeholder从小到大传参的,可以人为颠倒顺序

ref

  • 将拷贝的参数转换为引用
  • ref(val)

    hash哈希函数

  • hash<key_type>()函数为内置类型,指针,string和智能指针提供了哈希函数

智能指针shared_ptrunique_ptr

  • shared_ptr允许多个指针指向同一个对象
  • unique_ptr独占所指向的对象

    用法

  • 对指针可以直接做条件判断,true就是非空,否则是空
  • 最好不要用将自己申请的动态内存转化为智能指针的方式创建智能指针,可能会泄露
  • p.get()返回指针指向的对象
    • 注意,如果智能指针被释放,那么使用get返回的指针指向的对象也会被释放
  • shared_ptr比较是否相等时候,比较的是指向的对象是否相等,因此可以用来在STL中find()等等
  • shared_ptr基本用法
    #include <iostream>
    #include <memory>

    int main() {
    // 创建 shared_ptr,并分配一个整数
    std::shared_ptr<int> sharedInt = std::make_shared<int>(42);

    // 使用 shared_ptr
    std::cout << "Value: " << *sharedInt << std::endl;

    // 获取引用计数
    std::cout << "Reference count: " << sharedInt.use_count() << std::endl;

    // 创建另一个 shared_ptr,共享相同的整数
    std::shared_ptr<int> anotherSharedInt = sharedInt;

    // 引用计数增加
    std::cout << "Reference count: " << sharedInt.use_count() << std::endl;

    return 0;
    }
  • 另一种初始化
  • shared_ptr<int> p(new int(1024));可以用动态对象初始化指针
  • 但是必须显式的调用,不能隐式类型转换
  • 不要使用shared_ptrget方法为另一个智能指针赋值,会导致内存管理混乱
    • 比如另一个位置的shared_ptr析构,可能使得另一侧的引用计数为0,导致指向的对象直接被析构,但是这一侧的shated_ptr并不知情,再访问会导致段错误
  • unique_ptr基本用法
    #include <iostream>
    #include <memory>

    int main() {
    // 创建 unique_ptr,并分配一个整数
    std::unique_ptr<int> uniqueInt = std::make_unique<int>(42);

    // 使用 unique_ptr
    std::cout << "Value: " << *uniqueInt << std::endl;

    // unique_ptr 不能被拷贝,这会导致编译错误
    // std::unique_ptr<int> anotherUniqueInt = uniqueInt; // 错误!

    return 0;
    }
  • 可以用release或者reset转移指针的所有权
    • reset会释放本身指向的内存,将目标转移到函数传入的新地址上
    • release会返回自己指向的内存地址,同时放弃对这个位置的控制权(不会释放内存)
  • unique_ptr只有在即将被销毁(比如函数返回)或者是临时对象的时候可以被拷贝,接手自己的内存区域

    weak_ptr

  • 可以绑定到shared_ptr但是不影响引用计数
  • lock方法用于获取对应的shared_ptr,不存在则是空的指针对象
  • expired可以看当前是不是还有shared_ptr在指向空间

    Blob

  • 与vector类似,但是其中数据在拷贝的时候是公用的,不是像vector是复制的
  • 管理机制与共享指针类似,最后一个被释放之后存储的数据才会删除

tuple

  • 多种不同类型的组合
  • 注意make_tuple可以不显式的声明类型
    #include <tuple>
    std::tuple<int, float, std::string> myTuple(42, 3.14, "Hello");
    // 或者
    auto myTuple = std::make_tuple(42, 3.14, "Hello");
    //访问
    std::cout << "First element: " << std::get<0>(myTuple) << std::endl;
    std::cout << "Second element: " << std::get<1>(myTuple) << std::endl;
    std::cout << "Third element: " << std::get<2>(myTuple) << std::endl;
    // 解包为多个变量
    int intValue;
    double doubleValue;
    std::string stringValue;
    std::tie(intValue, doubleValue, stringValue) = myTuple;
    // 使用 std::tuple_size 获取 tuple 大小
    std::cout << "Tuple size: " << std::tuple_size<decltype(myTuple)>::value << std::endl;


bitset

  • 用于位运算的类
    #include <bitset>
    // 创建一个有 8 位的 bitset,初始值为 0
    std::bitset<8> myBitset1;

    // 用整数值初始化 bitset
    std::bitset<8> myBitset2(42); // 使用二进制表示为 00101010

    // 用字符串初始化 bitset
    std::bitset<8> myBitset3("10101010");
    // 访问位
    std::cout << "Bit at position 3: " << myBitset[3] << std::endl;

    // 修改位
    myBitset.set(1, true); // 将第 1 位设置为 1
    // set不给参数的话会默认为1
    // reset会把指定的位设置为0
    myBitset.flip(4); // 反转第 4 位

    // 位与
    std::bitset<8> resultAnd = bitset1 & bitset2;

    // 位或
    std::bitset<8> resultOr = bitset1 | bitset2;

    // 位异或
    std::bitset<8> resultXor = bitset1 ^ bitset2;
    // 获取 bitset 的大小
    std::cout << "Size of myBitset: " << myBitset.size() << std::endl;

    // 检查是否所有位都是 0
    std::cout << "All zeros? " << myBitset.none() << std::endl;

    // 检查是否有任意位是 1
    std::cout << "Any ones? " << myBitset.any() << std::endl;

    // 检查是否所有位都是 1
    std::cout << "All ones? " << myBitset.all() << std::endl;

lambda表达式

[capture list] (parameters) -> <return type>{};
  • 返回类型可以省略
  • 捕获的作用是比如函数作为谓词的时候,只能传递两个互相比较的参数,此时需要外部的参数就只能用捕获传入。
    • 全局变量不需要捕获也能使用
    • 可以捕获引用[&var]即可
    • 支持隐式捕获,也就是编译器自己推断捕获什么不捕获什么
    • [=, other vals]等号表示值捕获方式,可以与显式捕获混合使用
    • [&, other vals]引用符号表示引用捕获方式
    • 不同种类的捕获可以互相混用
    • 通过值捕获的变量一般不允许修改,除非在函数体之前加一个mutable关键字
  • lambda是函数类型,不是函数指针

函数对象

  • 包装器
  • 可以将任何可以调用的对象存入,能使用()的就可以,包装为一个函数
    #include <iostream>
    #include <functional>

    // 一个普通函数
    int Add(int a, int b) {
    return a + b;
    }

    // 一个函数对象
    struct Multiply {
    int operator()(int a, int b) {
    return a * b;
    }
    };

    int main() {
    // 使用 std::function 包装普通函数
    std::function<int(int, int)> addFunction = Add;
    std::cout << "Add Function Result: " << addFunction(3, 4) << std::endl;

    // 使用 std::function 包装函数对象
    std::function<int(int, int)> multiplyFunction = Multiply();
    std::cout << "Multiply Function Result: " << multiplyFunction(3, 4) << std::endl;

    // 使用 Lambda 表达式
    std::function<int(int, int)> lambdaFunction = [](int a, int b) {
    return a / b;
    };
    std::cout << "Lambda Function Result: " << lambdaFunction(8, 2) << std::endl;

    return 0;
    }

迭代器

反向迭代器

  • 倒序排序
    sort(vec.begin(), vec.end());
    sort(vec.rbegin(), vec.rend()); // 倒序

    iostream迭代器

  • 连续输入使用
  • 不初始化的话就是尾后迭代器
    istream_iterator<int> in_iter(cin);
    // 创建一个表示istream尾后位置的istream_iterator
    istream_iterator<int> eof;
    // 创建一个int类型的vector,并用in_iter和eof初始化
    vector<int> vec(in_iter, eof);

    // 读入一个元素并且赋值
    in_iter++;
    int y = *in_iter;
  • 输出流迭代器
    // 创建一个输出流迭代器,绑定到cout,数据类型为int,分隔符为","
    ostream_iterator<int> out_it(cout, ",");
    // 向输出流迭代器写入一个数据
    *out_it = 10;

    // 创建一个vector容器,存储一些int数据
    vector<int> vec = {1, 2, 3, 4, 5};

    // 使用copy算法,将vector中的数据复制到输出流迭代器中
    copy(vec.begin(), vec.end(), out_it);
  • 向输出流迭代器写入数据就是输出
  • 初始化的时候加的是每个数据之后的分隔符
  • *, ++等运算符对他没什么意义

    语法相关

  • 使用三目运算符?:的时候最好外加括号,否则会因为计算顺序的原因报错

    管理内存

  • newdelete是运算符一个分配动态对象一个删除动态对象

for_each

  • 作用是对一个区间内的变量每个都执行指定的操作(函数)
    #include <iostream>
    #include <vector>
    #include <algorithm>

    // 一个函数,用于在 for_each 中作为操作
    void PrintSquare(int x) {
    std::cout << x * x << " ";
    }

    int main() {
    // 创建一个向量
    std::vector<int> numbers = {1, 2, 3, 4, 5};

    // 使用 for_each 遍历向量并应用操作
    std::for_each(numbers.begin(), numbers.end(), PrintSquare);

    // 输出结果:1 4 9 16 25

    return 0;
    }

函数返回值

  • 不要返回局部对象的引用类型,否则会因为局部对象生命周期结束而失效
  • 返回指向某个数组的指针的函数
    int (*func(int i))[10];// 看的时候从里往外看,先返回的是指针,指针指向的是int[10]
    // 或者
    typedef int arr[10];
    arr* func(int i);
    // 或者
    auto func(int i) -> int(*)[10]
  • 也可以直接在返回值的位置使用decltype标出类型

Cpp函数重载

  • 函数重载只有在同一个级别的作用域才行
    • 假如在某个函数内部声明了一个与外部函数同名的函数,会导致外部同名函数被屏蔽
  • 假如有一组参数传入导致系统无法区分具体调用哪个的话会导致编译出错
  • 类型转换的分类
等级 转换
1 精确匹配
2 const转换实现匹配
3 类型提升实现匹配
4 算数类型转换实现匹配
5 类类型转换实现匹配

Cpp函数内联

  • 编译器可以选择忽略内联函数请求,也可以自动优化内联函数

是否是开发状态

  • 定义NDEBUG
  • 如果定义了就不能使用assert等debug用的工具
  • 也可以自己用#ifndef NDEBUG来控制自己写的代码哪些用在调试阶段

    调试输出

  • 可以用__func__打印当前函数的名字
  • __FILE__文件名
  • __LINE__行号
  • __TIME__编译时间
  • __DATE__编译日期

    backtracebacktrace_symbols函数

    #include <execinfo.h>

    int backtrace(void **buffer, int size);
  • backtrace函数是一个用于获取调用栈信息的函数,通常在调试或错误处理中使用。它在标准的C库中,头文件是#include <execinfo.h>
  • 使用backtrace_symbols函数将backtrace信息转换成字符串
  • char **bt_strings = backtrace_symbols(bt_buffer, bt_size);
    #include <execinfo.h>
    #include <stdio.h>
    #include <stdlib.h>

    void printStackTrace() {
    const int max_frames = 16;
    void *addrlist[max_frames + 1];
    int addrlen = backtrace(addrlist, sizeof(addrlist) / sizeof(void *));

    if (addrlen == 0) {
    fprintf(stderr, "No stack trace available.\n");
    return;
    }

    char **symbolList = backtrace_symbols(addrlist, addrlen);
    if (symbolList == NULL) {
    perror("backtrace_symbols");
    exit(EXIT_FAILURE);
    }

    printf("Stack trace:\n");
    for (int i = 0; i < addrlen; i++) {
    printf("%s\n", symbolList[i]);
    }

    free(symbolList);
    }

    void foo() {
    printStackTrace();
    }

    void bar() {
    foo();
    }

    int main() {
    bar();
    return 0;
    }
  • backtrace函数在编译时需要开启调试信息。如果使用gcc编译,可以添加-g
  • 传入的需要是一个指针数组或者是使用addrlist = (void **)malloc((sizeof(void *) * addrlen));分配好内存的空间

    函数指针

  • 给函数指针赋值的时候加不加取地址符号都一样
    <return type> (*func name)(<para> parameters);
  • 不同种类的函数指针之间无法转换,即使形参列表能转换,也就是说必须是返回类型形参列表与自身完全一样的函数才可以赋值给函数指针
  • 但是调用的时候可以发生类型转换
  • 返回的时候不能返回函数,因为函数不能拷贝,只能返回函数指针

    如何返回函数指针

  • int (*f1(int))(int, int);返回的是一个int(*)(int, int)类型的函数指针
  • 或者写auto f(int) -> int(*)(int, int)

动态内存管理

动态数组

  • int*a = new int[10];是未初始化的
  • int*a = new int[10]();是已经值初始化为0的
  • int* a = new int[3]{0, 1, 2};是显式初始化列表的
  • 创建长度为0的动态数组可以,但不能解引用
  • 释放的时候必须用delete [] ptr

allocator类申请内存

#include <iostream>
#include <memory>

int main() {
// 使用 std::allocator 分配一块内存来存储 int 类型的元素
std::allocator<int> allocator;

// 分配内存以存储一个 int
int* ptr = allocator.allocate(1);

// 在分配的内存上构造 int 对象
allocator.construct(ptr, 42);

// 使用构造的对象
std::cout << "Value: " << *ptr << std::endl;

// 销毁对象并释放内存
allocator.destroy(ptr);
allocator.deallocate(ptr, 1);

return 0;
}
  • alloccate仅仅是分配内存
  • construct才构造对象
  • 删除的时候先destroy
  • 然后再解除内存占用deallocate
  • 使用allocator构造内存的时候可以像使用指针一样对指针做加减,只要不超出构造的范围即可

    一次初始化多个元素

  • `std::uninitialized_fill(intPtr, intPtr + 3, 1);
    • 注意,uninitialized_fill的第二个参数类似于尾后迭代器,不会真的被赋值

面向对象

成员函数

  • 常量成员函数<return type> func() const {}

    • 不修改类对象的函数
    • 对这个类的const实例的时候会调用const的成员函数
    • 但是假如某个成员有mutable关键字,那么也可以修改

      访问说明符

  • publicprivate可以反复出现多次

    友元

  • 类内声明

  • friend <return type> func();

  • 即使这个类被内嵌在一个子类中,也可以访问相应部分的成员

    不同类别的继承

  • private继承会把所有从基类继承的成员都作为private对象

  • protected会把所有的作为protected

  • public则不改变权限

  • 可以手动在public、protected或者private后面使用using 基类名:: 基类成员手动变更访问权限

  • 继承的时候子类的同名成员会覆盖基类的,无论是变量还是成员函数

    作用域运算符::

  • 访问全局作用域中的变量直接用::<variable>即可

  • 访问类的就用<class name>::<var>

    构造函数

  • 子类在构造的时候必须在初始化列表中显式的调用基类的构造函数构造基类的部分

  • 派生类构造函数只能通过初始化列表或构造函数的成员初始化列表来调用基类构造函数,而在函数体中是无法再次调用的

    类成员初始化

  • 无论初始化列表里怎么写,类成员的初始化顺序是按照他们在类中定义的顺序的

    委托构造函数

  • 类的多个构造函数中一个构造函数借用类的其他功能强大的构造函数构造对象

    class test
    {
    string data;
    public:
    test(string s):data(s){}
    test():test("Hello World"){} // 这里是委托带参数的构造函数test(string s)构造对象
    }
  • 调用默认构造函数(无参数)的时候不能加括号,否则编译器会认为你想声明一个函数

    className obj();// 看起来就是声明一个叫obj的函数

    拷贝构造函数

  • className(const className& cName){};

  • 第一个参数必须是自身的引用

  • 其他参数必须有默认值

  • 注意形如<className> val = valOld;的是拷贝初始化

    移动构造函数

  • 移动语义的目的是在避免不必要的数据复制的同时,更高效地管理资源。

  • 在 C++11 引入右值引用之前,对象的拷贝构造函数是唯一的构造函数,用于复制对象。然而,对于临时对象或即将销毁的对象,进行深拷贝可能是不必要的开销。移动构造函数通过使用右值引用,允许在不复制底层资源的情况下将资源从一个对象“移动”到另一个对象。

  • 必须是noexcept标明的

  • 传入的参数是一个右值引用

  • 有不具备移动构造功能的成员,自身也不能移动构造

    #include <iostream>
    #include <string>

    class MyString {
    public:
    // 移动构造函数
    MyString(MyString&& other) noexcept
    : data(other.data), length(other.length) {
    std::cout << "Move Constructor" << std::endl;
    // 移动构造函数中,将原对象的资源指针置为空,避免资源被析构
    other.data = nullptr;
    other.length = 0;
    }

    // 构造函数
    MyString(const char* str)
    : data(strdup(str)), length(strlen(str)) {
    std::cout << "Constructor" << std::endl;
    }

    // 析构函数
    ~MyString() {
    std::cout << "Destructor" << std::endl;
    // 释放资源
    free(data);
    }

    private:
    char* data;
    std::size_t length;
    };

    int main() {
    // 创建一个对象
    MyString source("Hello, World!");

    // 调用移动构造函数,将资源从 source 移动到 target
    MyString target = std::move(source);

    // 输出结果
    std::cout << "Source String: " << (source.GetData() ? source.GetData() : "nullptr") << std::endl;
    std::cout << "Target String: " << (target.GetData() ? target.GetData() : "nullptr") << std::endl;

    return 0;
    }

    运算符重载

  • 可以是成员函数也可以直接是函数

  • 如果运算符是某个类的成员函数那么左侧的操作数必须是这个类的对象

  • 重载输入输出运算符的时候必须不是成员函数

  • 下标运算符必须是成员函数

  • 赋值运算符必须是成员函数

    自增自减运算符

  • className& operator++();前置

  • className& operator(int); 后置,参数无用,用于与前置区分

  • 一般是成员函数

重载->运算符

  • 返回值必须是指针(可以是某个成员函数类型的指针
    #include <iostream>

    class MyClass {
    public:
    void Display() const {
    std::cout << "Displaying MyClass" << std::endl;
    }

    // 重载 -> 运算符
    MyClass* operator->() {
    return this;
    }
    };

    int main() {
    MyClass myObject;

    // 使用重载的 -> 运算符
    myObject->Display();

    return 0;
    }

等号运算符

  • className& operator=(const className&);
  • 控制等号的行为
  • return *this;即可
  • 注意处理等号左右是同一个变量的情况

要求系统合成一个默认的构造/析构函数

  • ClassName() = default;
  • =default;

    禁止一个构造/复制函数被调用

  • ClassName &operator=(const ClassName&) = delete;
  • =delete;
  • 可以对于任何函数使用
  • 删除析构函数的类型,不能定义该类型的变量或者释放该类型动态对象的指针

类型转换

  • 如果类具有只接受一个参数的构造函数,那么编译器可以将这个参数类型的对象自动转换为这个类的对象
    • 给构造函数增加一个explicit参数可以禁止这种转换
    • 但是static_cast可以调用explicit的构造函数

重载类型转换运算符

  • 类型转换运算符通常不显式声明返回类型,因为它们没有返回类型
    #include <iostream>

    class Distance {
    private:
    int feet;
    float inches;

    public:
    Distance(int ft, float in) : feet(ft), inches(in) {}

    // 类型转换运算符,将 Distance 转换为 float
    operator float() const {
    return static_cast<float>(feet) + inches / 12.0f;
    }
    };

    int main() {
    Distance d(5, 9.0);

    // 使用类型转换运算符将 Distance 转换为 float
    float totalInches = static_cast<float>(d);

    std::cout << "Total Inches: " << totalInches << std::endl;

    return 0;
    }
  • 注意防止类型转换运算符与某些具有隐式类型转换功能的构造函数出现二义性
  • 只要有多个用户自定义的类型转换都能达到目的,编译器就认为出线了二义性

override

  • override的主要作用是在子类中表明某个函数是覆盖基类的虚函数的,方便检查是否真的覆盖了

动态绑定

  • 使用一个基类的指针调用一个虚函数
  • 根据不同的子类对虚函数的继承,产生不同的结果
  • 使用基类的指针调用子类的时候,必须基类也有这个函数才行
  • 使用智能指针也能实现这些功能

多态和虚函数

  • 使用基类的指针(或引用)不能直接访问子类中定义了但是基类中没定义的成员函数和变量
    • 需要使用类型转换才能访问
  • 但是可以访问子类中重载了的基类的虚函数
  • 虚函数继承的时候必须函数头完全一致才行,这样才能实现多态
    • 不一样的话会直接覆盖基类的虚函数,不会产生多态,用基类的指针的时候还是会调用基类的函数
  • 可以使用类名和作用域运算符手动指定通过指针调用的是哪个函数,从而防止运行时才知道是哪个
  • 基类的构造函数中无法访问子类重载的虚函数,因为这个虚函数表是之后才被构建出来的

    析构函数

  • 基类的析构函数最好写成虚函数,方便多态调用的时候析构掉整个的,否则析构基类的动态对象的时候会只析构基类的部分导致内存泄漏
  • 具有手动析构函数的类,编译器一般不会自动合成移动构造函数、拷贝构造函数、等号运算符重载等,需要的话最好手动指出
    • 否则会因此导致继承他的子类因为父类没有这些函数而无法复制构造

      基类的拷贝构造函数和移动构造函数

  • 必须显式的子类的相应函数中的初始化列表中调用基类的拷贝构造函数或者移动构造函数
    • picture 0

基类和派生类的=赋值运算符

  • 需要在派生类的=赋值运算符函数体中手动调用基类的赋值运算符

构造函数虚函数的情况

  • 在派生类的构造函数初始化列表中调用基类的构造函数的时候最好手动通过类名::的形式指定需要调用的函数,防止因为多态导致调用了继承的构造函数

智能指针的情况

  • 涉及智能指针的情况也可以实现多态
  • make_shared或者其他构造一个子类对象即可

抽象类和纯虚函数

  • 在一个成员函数后面加上=0;即可实现
  • 类不能被实例化
  • 纯虚函数的定义必须在外部

集成的时候的构造函数

  • 基类的数据成员必须调用基类的构造函数构造(在初始化列表直接调用)
  • 然后再按声明的顺序构造自身的对象

不能被继承的类

  • 类声明之后加一个final

privateprotected

  • protected是继承的类能拿到但是别的类拿不到
  • private继承的类也拿不到
  • 派生类的友元对基类的保护对象访问没有任何特权,无法访问protected

聚合类

  • 所有成员public
  • 没有构造函数
  • 类内没给初始值
  • 没有基类和虚函数
  • 可以用花括号直接初始化,类似于struct

    字面值类

  • 数据成员必须都是字面值类型
  • 至少有一个常量表达式的构造函数
  • 析构函数必须是默认的
  • 内置类型必须是常量表达式,类调用自己的常量构造函数
  • constexpr的构造函数的函数体必须是空的
  • 此时可以在构造的时候加一个constexpr

    静态成员

  • static
  • 对象的内存不包含静态对象
  • static函数不含有this指针,不能处理非static成员
  • 可以使用类作用域直接调用,也可以用对象的.调用
  • static关键字只出现在类内声明的场合,定义的时候不出现
  • 初始化
    • 只有是constexpr的情况下可以在类内声明,但是无法在外部使用
    • int <class name>:: var = blabla;
  • 可以在一个类还没被初始化完的时候就声明它的静态对象
  • 类的(不一定是静态)成员函数操作静态成员的时候可以直接操作,不需要加作用域运算符

IO流对象

  • iostream, fstream, sstream
  • 不能修改,赋值
  • 输出endl是刷新缓冲区附带一个回车,输出flush则只刷新缓冲区,输出ends刷新缓冲区的同时附带一个空格
  • 输入和输出流可以绑定,多个输入可以绑定到一个输出
    • .tie(<some stream>)函数
    • 输入与输出绑定的时候,调用输入流的时候自动刷新输出缓冲区
  • fstream可以用open或者close打开或者关闭文件
  • 析构的时候会自动close文件

    打开方式

    open函数flag|功能
    |—|—|
    in|读
    out|写
    app|追加(每次操作都从末尾开始)
    ate|打开的时候定位到结尾
    trunc|如果存在,丢弃内容
    binary|二进制
  • 只有app和in方式可以保留原来的内容
  • 不指定的话是outtrunc

    sstream

  • 自身就是个string
  • 可以初始化也可以不初始化
    #include <iostream>
    #include <string>
    #include <sstream>
    using namespace std;

    int main()
    {
    // 将一个整数转换为string
    int n = 123;
    stringstream ss1;
    ss1 << n; // 向stringstream写入整数
    string s1 = ss1.str(); // 获取stringstream内部的string
    cout << "s1 = " << s1 << endl; // 输出s1

    // 将一个string分割为单词
    string s2 = "Hello world!";
    stringstream ss2(s2); // 用string初始化stringstream
    string word;
    while (ss2 >> word) // 从stringstream读取单词
    {
    cout << "word = " << word << endl; // 输出单词
    }

    return 0;
    }

std::istringstream

  • 一个字符串输入流,允许你从字符串中读取数据,就像从标准输入流中读取数据一样
    #include <iostream>
    #include <sstream>
    #include <string>

    int main() {
    std::string inputString = "123 4.56 Hello";

    // 创建 istringstream 对象,并将字符串传入
    std::istringstream iss(inputString);

    int intValue;
    float floatValue;
    std::string stringValue;

    // 从字符串流中读取数据
    iss >> intValue >> floatValue >> stringValue;

    // 输出读取到的数据
    std::cout << "Int: " << intValue << ", Float: " << floatValue << ", String: " << stringValue << std::endl;

    return 0;
    }

    std::ostringstream

  • 一个字符串输出流,允许你将数据写入字符串,就像写入标准输出流一样
    #include <iostream>
    #include <sstream>
    #include <string>

    int main() {
    int intValue = 42;
    float floatValue = 3.14;
    std::string stringValue = "Hello, World!";

    // 创建 ostringstream 对象
    std::ostringstream oss;

    // 将数据写入字符串流
    oss << "Int: " << intValue << ", Float: " << floatValue << ", String: " << stringValue;

    // 获取字符串
    std::string resultString = oss.str();

    // 输出写入的字符串
    std::cout << resultString << std::endl;

    return 0;
    }

iostream格式化

int num = 42;
// 进制
std::cout << "Default: " << num << std::endl;
std::cout << "Hexadecimal: " << std::hex << num << std::endl;
std::cout << "Octal: " << std::oct << num << std::endl;
std::cout << "Decimal: " << std::dec << num << std::endl;
//填充字符
std::cout << "Default fill: " << std::setw(10) << num << std::endl;
std::cout << "Fill with '*': " << std::setfill('*') << std::setw(10) << num << std::endl;
//对齐
std::cout << "Default alignment: " << std::setw(10) << num << std::endl;
std::cout << "Left alignment: " << std::left << std::setw(10) << num << std::endl;
std::cout << "Right alignment: " << std::right << std::setw(10) << num << std::endl;
std::cout << "Internal alignment: " << std::internal << std::setw(10) << -num << std::endl;
// 精度
double pi = 3.141592653589793;
std::cout << "Default precision: " << pi << std::endl;
std::cout << "Precision 4: " << std::setprecision(4) << pi << std::endl;
// 设置宽度
std::cout << "Width 10: " << std::setw(10) << num << std::endl;

iostream其他用法

  • cin.get
    int cin.get();
    istream& cin.get(char& var);
    istream& get ( char* s, streamsize n );
    istream& get ( char* s, streamsize n, char delim )
  • cin.getline
    istream& getline(char* s, streamsize count); //默认以换行符结束
    istream& getline(char* s, streamsize count, char delim);
  • gets
  • gets是C中的库函数,在<stdio.h>申明,从标准输入设备读字符串,可以无限读取
  • 不会判断上限,以回车结束或者EOF时停止读取
    #include <iostream>
    using namespace std;
    int main()
    {
    char array[20]={NULL};
    gets(array);
    cout<<array<<endl;
    system("pause");
    return 0;
    }

    模板

  • 声明模板类型
  • template<typename T>或者template<typename T, class U>
  • classtypename相同,都可以, 但每个类型钱都必须有这二者之一

声明未定大小的数组

  • template<unsigned N, unsigned M>其中N和M是未定的大小,比如用于
    template<unsigned N, unsigned M>
    int compare(const char (&p1)[N], const char (&p2)[M]);

    类模板和类模板的成员函数

    #include <iostream>

    // 类模板定义
    template <typename T>
    class SimpleTemplate {
    private:
    T data;

    public:
    // 构造函数
    SimpleTemplate(T value) : data(value) {}

    // 成员函数
    void display() {
    std::cout << "Data: " << data << std::endl;
    }

    // Getter 函数
    T getData() const {
    return data;
    }

    // Setter 函数
    void setData(T value) {
    data = value;
    }
    };
  • 在类外定义类的成员函数注意也要写模板参数
    #include <iostream>

    // 类模板的声明
    template <typename T>
    class MyClass {
    private:
    T data;

    public:
    // 构造函数的声明
    MyClass(T value);

    // 成员函数的声明
    void display() const;
    };

    // 构造函数的定义
    template <typename T>
    MyClass<T>::MyClass(T value) : data(value) {}

    // 成员函数的定义
    template <typename T>
    void MyClass<T>::display() const {
    std::cout << "Data: " << data << std::endl;
    }
  • 一个模板类的成员函数在用到的时候才被实例化
  • 在一个类模板的作用域内,不必在类名之后带<T>
    template <typename T>
    retType className<T>::func(val)
    {
    // 这里使用类的时候直接用className就行
    }

    类模板的友元函数

  • 只有实例化为同一种类型才成立的友元
    #include <iostream>

    // 前置声明
    template <typename T>
    class MyClass;

    // 友元类模板的特例化
    template <typename T>
    class FriendClass {
    public:
    // 友元函数模板的特例化
    friend void FriendFunction(const MyClass<T>& obj);
    };

    // 类模板的声明
    template <typename T>
    class MyClass {
    private:
    T data;

    public:
    // 构造函数
    MyClass(T value) : data(value) {}

    // 友元声明
    friend class FriendClass<T>;

    // 成员函数
    void display() const {
    std::cout << "Data: " << data << std::endl;
    }
    };
  • 可以通过在friend函数前面独立增加一个template <typename X>来打破友元必须与类实例化为相同的类型才能成为友元的要求,从而使得所有情况下友元都成立

    模板类型别名

    template <typename T> using twin = pair<T, T>;
    twin<string> authors; // 相当于`pair<string, string>`

模板类的静态成员

  • 不同实例化的模板类不共享静态数据成员,只有相同实例化的共享
    // 初始化静态数据成员
    template <typename T>
    retType ClassName<T>:: val - 0;

    模板默认参数

  • template <typename T = int>
  • 希望使用默认模板参数的时候使用<>即可但是不能省略

模板参数推导

  • 编译器会默认进行模板参数推导,比如调用模板函数的时候,编译器会根据调用的方式自动推导函数模板实例化成哪种函数
  • 用给好类型的函数指针指向模板函数的时候,编译器也会根据要赋值的函数指针的类型推断模板函数的实例化方式

需要置顶尾置返回类型的场合

  • 需要使用模板参数结合decltype推断返回类型的场合
    template <typename T, typename U>
    auto add(T t, U u) -> decltype(t + u) {
    return t + u;
    }

模板函数的重载

  • 用别的模板函数重载
    #include <iostream>

    // 模板函数接受一个模板参数
    template <typename T>
    void myFunction(T value) {
    std::cout << "Template Function with one template parameter: " << value << std::endl;
    }

    // 重载模板函数,接受两个模板参数
    template <typename T, typename U>
    void myFunction(T t, U u) {
    std::cout << "Template Function with two template parameters: " << t << ", " << u << std::endl;
    }
  • 用非模板函数重载
    #include <iostream>

    // 模板函数
    template <typename T>
    void myFunction(T value) {
    std::cout << "Template Function: " << value << std::endl;
    }

    // 非模板函数
    void myFunction(int value) {
    std::cout << "Non-Template Function: " << value << std::endl;
    }
  • 匹配的原则是谁更特殊就匹配谁,非模板版本优先。谁的范围更窄,先匹配谁

可变参数类型的类模板

#include <iostream>

// 模板递归终止条件
void printValues() {
std::cout << "End of recursion" << std::endl;
}

// 可变模板参数的模板函数
template <typename T, typename... Args>
void printValues(T first, Args... args) {
std::cout << "Value: " << first << std::endl;
printValues(args...); // 递归调用
}

int main() {
printValues(1, 3.14, "Hello", 'A');

return 0;
}
  • Args... 表示一个参数包,可以接受零个或多个类型。在递归调用中,args... 用于展开参数包,使得每个参数都能够被单独处理
    • 就是每次递归调用的时候都把第一个赋值给fisrt,后面的还在args...内部
  • 使用sizeof...计算参数数量
    #include <iostream>

    // 可变模板参数的模板函数
    template <typename... Args>
    size_t countArgs(Args... args) {
    return sizeof...(args);
    }

    int main() {
    std::cout << "Number of arguments: " << countArgs(1, 3.14, "Hello", 'A') << std::endl;

    return 0;
    }

    模板函数特例化

    #include <iostream>

    // 通用模板函数
    template <typename T>
    void myFunction(T value) {
    std::cout << "Generic Template Function: " << value << std::endl;
    }

    // 模板特化 - 针对int类型
    template <>
    void myFunction<int>(int value) {
    std::cout << "Specialized Template Function for int: " << value << std::endl;
    }
  • 就是专门为T是某种特定的类型而设定的版本

错误处理

#include <iostream>
#include <stdexcept>

int divide(int a, int b) {
if (b == 0) {
throw std::runtime_error("Division by zero!");
}
return a / b;
}

int main() {
try {
int result = divide(10, 0);
std::cout << "Result: " << result << std::endl;
} catch (const std::exception& e) {
std::cerr << "Exception caught: " << e.what() << std::endl;
}

return 0;
}
  • 写了noexcept的函数抛出异常不会被catch,会导致程序崩溃
    • noexept(false)表示可能抛出异常
    • 反之认为不可能抛出异常

自定义异常

#include <stdexcept>
#include <string>

class MyException : public std::exception {
private:
std::string errorMessage;

public:
MyException(const std::string& message) : errorMessage(message) {}

const char* what() const noexcept override {
return errorMessage.c_str();
}
};

命名空间

// 定义命名空间
namespace MyNamespace {
int globalVar = 42; // 命名空间中的全局变量

void printMessage() {
std::cout << "Hello from MyNamespace!" << std::endl;
}

class MyClass {
public:
void display() {
std::cout << "Inside MyClass" << std::endl;
}
};
}
  • 可以不连续,namespace name{}可以在文件中反复出现多次
  • 别名namespace A = B;