2. cpp语法进阶

sizeof

如果后面跟类型名,则必须加括号,如sizeof(sockaddr_in),告知编译器这是类型

cpp 复制代码
sizeof int *p; // 歧义:是 (sizeof int) * p,还是 sizeof(int *)

如果后面跟变量名,则都可以sizeof(a)sizeof a

const

普通变量

  1. 如果在编译时就确定了值,此时普通变量成了常量,编译过程中,把出现常量名字的地方,用常量的值进行替换
cpp 复制代码
const int a = 10;
int *p = (int *)&a;//此处就必须给a分配内存
*p = 20;
std::cout << a << " " << *p << std::endl;
//10 20,会在编译时就将a变为10
  1. 宏定义是在预处理的时候直接进行替换,不会存在类型、空间和作用域等问题,但是常量会有空间(可能被优化掉)、类型和作用域

指针或引用

  1. 如果修饰的是指向的对象,则对象的非静态数据成员(除mutable修饰的成员变量)自身都是不可以变化的;该对象只能调用const修饰的成员方法
  • 因为传入的this指针的实参有const修饰,所以this也必须有const修饰
  • const this指向的对象中如果有引用或指针,则const修饰的是变量,而不是底层指向的变量对象,其实对引用并没有约束力
cpp 复制代码
struct ClosureType {
    int x;
    int* z;
    int& y;  // 按引用捕获的变量
    int operator()() const {
        // 此时等价于int* const z;
        y++;  // 允许:引用本身就等价于int* const y;
        return x;
    }
};

成员函数

  1. 当修饰成员函数时,其实修饰的是传入的this指针 ,因此也是重载,注意普通函数不能使用const修饰
  2. 不会限制通过引用或指针修改成员变量指向的对象,见上面的代码
cpp 复制代码
void fun(int a,int b) const{}//方法一
void const fun(int a,int b){} //方法二
// 这两种写法的本质是:void fun (const 类 *this, int a,int b);

mutable

  1. 修饰非静态成员变量,核心作用是突破 const 成员函数的限制,能在const修饰的成员函数中修改,使用场景在于锁
  2. 修饰lambda函数,则是将lambda对应的函数对象重载的operator()的默认const属性去掉,使得能修改按值捕获的外部变量
cpp 复制代码
class ThreadsafeCounter{
    mutable std::mutex mtx;
    int value;
public:
    int get() const{
        // 依然可以修改
        std::lock_guard<std::mutex> lock(mtx);
        return value;
    }
}

指针

指向类成员的指针

增加了作用域限定

静态

属于类,已经存在于常量或代码区,所以可以像普通指针初始化和使用

非静态

属于实例对象,所以必须与对象结合

MemberType ClassName::*ptr = &ClassName::member;

指向的是作用域ClassName内部的MenberType类型的指针
ReturnType (ClassName::*ptrName)(Type1,Type2...) = &ClassName::Function

指向的是作用域ClassName内部的函数指针

cpp 复制代码
#include <stdio.h>
#include <string>
class Test
{
public:
    void print() { printf("ma:%d\n", ma); }
    int ma;
    static void staticPrint(std::string str)
    {
        printf("static call %s ,static mb %d\n", str.c_str(), mb);
    }
    static int mb;
};
int Test::mb = 0;

int main()
{
    Test t1;
    void (Test::*ptrFunc)() = &Test::print;
    int Test::*ptr = &Test::ma; // 注意初始化方式
    t1.*ptr = 20;               // 注意访问方式
    (t1.*ptrFunc)();

    int *staticPtr = &Test::mb;  // 初始化方式不一样,左边不需要作用域限定
    void (*staticPrint)(std::string) = &Test::staticPrint;
    *staticPtr = 30;             // 使用时也不需添加对象限定
    (*staticPrint)("hello");
}

函数

函数参数匹配

  1. 调用 构造函数 进行隐式转换(如果可用)。
  2. 调用 类型转换运算符 (如 operator int())。
  3. 如果无法转换,则查找下一个候选函数。

尾置返回类型

  1. auto func(params) -> ReturnType{}
  2. \]()`->ReturnType`{}

无捕获lambda可以转换给函数指针,底层借助重载类型转换和静态方法实现

cpp 复制代码
auto lambda = [](int a, int b) { return a + b; };
int (*funcPtr)(int, int) = lambda; // 隐式调用 operator int (*)(int, int)()

// 编译器实际生成以下内容:
// 1. 生成一个匿名类
class __Lambda_123 {
public:
    int operator()(int a, int b) const { 
        return a + b; 
    }
    // 关键:隐式转换为函数指针的运算符
    operator int (*)(int, int)() const { 
        return static_cast<int (*)(int, int)>(&__Lambda_123::__invoke);
    }
private:
    // 2. 生成一个静态成员函数(实际执行 Lambda 体的函数)
    static int __invoke(int a, int b) {
        return a + b;
    }
};

// 3. 实例化 Lambda 对象
__Lambda_123 lambda;

函数重载

函数重载需要函数名、作用域相同,而参数不同(类型、数量、顺序)

  1. 这里参数类型包括了普通类型、引用类型和指针类型之间的不同,如int aint &a会构成重载
  2. 当const修饰指向的对象时,也会构成重载,如int& aconst int& a会构成重载;
  3. 但是如果只是修饰的变量,则不构成重载,如int* const aint *a 构成重载

运算符重载

运算符重载的查找顺序

  1. 类内重载(成员函数形式) Cpp class MyClass { public: MyClass operator+(const MyClass& other); // 类内重载 + };
  2. 全局重载(非成员函数形式) Cpp MyClass operator+(const MyClass& a, const MyClass& b); // 全局重载 +
  3. 默认行为 (如内置类型的 +
  4. 如果都不匹配,报错

特殊运算符

1. operator++存在前置和后置

尽量使用前置 ++ (避免临时对象拷贝)

  • operator++() 为前置
  • operator++(int) 代表后置,会有临时变量拷贝并返回临时变量
2. 类型转换运算符 operator T()

定义对象到其他类型的隐式转换,没有返回值类型,因为函数名就是返回值类型

cpp 复制代码
class MyInt { 
    int val; 
public: 
    operator int() const { 
        return val; 
    } // MyInt 可隐式转为 int 
}; 
MyInt x; 
int y = x; // 隐式调用 operator int()
3. 流运算符operator<<|operator>>

注意返回类型和参数

cpp 复制代码
class Date { 
    int year, month, day; 
    friend ostream& operator<<(ostream& os, const Date& d); 
}; 
ostream& operator<<(ostream& os, const Date& d) { 
    return os << d.year << "-" << d.month << "-" << d.day; 
}
4. 函数调用运算符 operator()

使得对象能像函数一样被调用

5. 下标运算符operator[]

类似数组的访问方式,必须返回引用,以支持修改 如果不希望修改,可以返回const 引用

6. 成员访问运算符 operator->

用于智能指针,必须返回原来的指针,否则可能出现无限递归调用

arduino 复制代码
class SmartPtr { 
    int* ptr; //原本的指针
public: 
    int* operator->() { return ptr; } 
    int& operator*() { return *ptr; } 
};

// smartPtr->a等价于ptr->a
7.operator new、delete、new[]、delete[]
  1. 操作符new是无法重载的,但是operator new是可以重载的,operator new只是负责分配内存,当使用操作符new时会调用这个函数,然后由编译器调用构造函数再进行隐私转换成对应指针 所以咱们重构operator new等函数,就是为了分配内存,当涉及较多增删时,可以使用内存池,一开始就分配很多个对象,需要时直接取下即可
  2. 当使用new[]分配自定义类型时,编译器会在最前面增加四个字节,来记录分配了多少个,告知之后调用析构函数多少次,返回的确是这四字节之后的地址,在重载new[]不需要写出
  3. delete[]释放自定义类型时,会释放前面四个字节,所以在对于自定义类型时,要注意对应关系
  4. 对于基本类型,由于不需要调用析构函数,所以不会分配之前的四字节,也就可以混用,但是不推荐
  5. malloc是按字节分配,所以传入是字节数,传出是void*,而new是按类型T分配,返回的就是T*,free传入的是malloc分配的首地址
  6. 定位new ,在预先分配(malloc)的内存上构造函数:new(memory) T(arg...);但是这样之后无法调用delete,而需要手动调用析构函数
  7. 在类中实现重载,为静态方法 ,即使没有添加static,因为此时都没有对象产生!!!只在调用new 该类时才会调用此重载函数
cpp 复制代码
// 单对象版本 
void* operator new(size_t size); // 普通 new 
void operator delete(void* ptr) noexcept; // 普通 delete 
// 数组版本 
void* operator new[](size_t size); // new[]
void operator delete[](void* ptr) noexcept; // delete[]
// 定位new
void* operator new(size_t size, void* ptr); // placement new,不涉及开辟内存,直接返回ptr即可

模板

函数模板

  1. 函数模板是不进行编译的!!!在函数调用点进行实例化成为模板函数,这个才是被编译器所编译的,连接器要找的也就是这个。所以尽量把函数模板与调用点置于同一个cpp文件或头文件中,否则可能因为调用点没有编译出实例化代码,导致后续链接出错

  2. 实例化可以通过

  • 显示指定function<Type>(Type arg1...)
  • 实参推演function(Type arg1...),但是参数类型总数要小于模板类型参数
  • 特例化(不支持部分特例化,可能导致歧义)
cpp 复制代码
// 特例化
template<>
bool compare<const char*>(const char* a,const char* b){
    return strcmp(a,b)>0;
}
  1. 模板函数compare<Type>(arg...)才是函数符号,所以与compare(arg...)不算真正构成重载,但是编译器依然会将两者优先级进行比较
  2. 重载决议优先级:普通函数 > 特例化模板 > 通用模板实例
  3. 非类型参数必须是(int/size_t)整数类型template<typename T,int SIZE>,传入时使用常量整型(地址或引用也可以)
  4. 实参推演加上库typeinfo中的typeid().name()获得运行时类型
cpp 复制代码
#include <typeinfo>
template<typename R,template T,template A1,template A2>
void func(R(T::*a)(A1,A2)){
    // 传入函数非静态成员方法,能获得所有类型
    cout<<typeid(R).name()<<endl;
    cout<<typeid(T).name()<<endl;
    cout<<typeid(A1).name()<<endl;
    cout<<typeid(A2).name()<<endl;
}

类模板

  1. 类模板--》实例化--》模板类className<T>
  2. 在使用类时,注意类名为className<T>,构造、析构函数可以省略后面的<T>,类内部使用类型时也可省略,但是如果在类外部,就必须使用模板类类名
  3. 成员函数代码只会在调用时才生成(即实例化)
  4. 可以给参数类型设置默认值template<typename T = int>,在调用时可以className<> a即可
  5. 部分特例化
cpp 复制代码
// 类模板 #3
template<typename T>
class Vector
{
    Vector(){};
}
// 完全特例化 #1
template<>
class Vector<char *>
{
    Vector(){};
}
// 部分特例化,如只针对指针类型 #2
// 此指针能匹配函数指针,此时T是函数类型
template<typename T>
class Vector<T*>{}

// 有两个参数的函数指针的部分特例化,
// 如果是函数指针,优先匹配到此
template<typename R,typename A1,typename A2>
class Vector<R(*)(A1,A2)>{}

可变模板编程

cpp 复制代码
template<typename... Args> 
class tuple;  // 类模板声明

template<typename... Args>
void func(Args... args);  // 函数模板声明
  1. args为一个参数包,包含0到任意个模板参数,无法直接获得里面的参数
  2. sizeof...运算符,sizeof...(args)统计函数参数数量,Args则是统计类型参数数量
  3. 参数包展开,以获取里面的参数
  1. 递归函数展开,函数包展开函数+递归终止函数,比较麻烦
cpp 复制代码
template<typename T>
T sum(T t) {
    return t;
}

template<typename T, typename... Types>
T sum(T first, Types... rest) {
    return first + sum(rest...);
}

sum(1,2,3,4); // 返回10
  1. 在合适上下文中展开,如初始化列表、函数传参等使用expr(args)...,以及模板参数列表(如 Template<Args...>
cpp 复制代码
int arr[] = { (func(std::forward<Args>(args)), 0)... };//这样就不需要递归终止函数了
//生成`int arr[] = {0,0,0,0}`,作用在于提供上下文

std::bind(func, std::forward<Args>(args)...)
// 这里就有模板参数列表、函数列表

强制类型转换

  1. 类型的作用是赋予内存中的二进制数据(0和1的组合)特定的解释规则和操作方式 。例如,同样的4字节数据,若声明为int类型,CPU会将其视为补码形式的整数;若声明为float类型,则会按IEEE 754标准解析为浮点数。
  2. 赋值从硬件层面看就是内存中的二进制迁移到另一块内存中,而编译器会借助类型判断这样做是否合法(隐式转换),可以人为干预编译器(强制类型转换)

1. 类c

用法:

cpp 复制代码
double a =3;
int b = (int)a;//法一
int c = int(a);//法二

缺点: 由于不受限制,编译器并不会进行检查 ,会出现不同类指针可以随意变换,甚至非指针变换为指针类型(char *)666,导致最后运行时出错,但是编译出错的代价远小于运行时出错,所以不推荐此方法

2. cpp方式

  • static_cast:编译时检查,用于常规转换,即基本类型之间转换、类的向上转换(大-->小),当然也支持派生转基类
cpp 复制代码
int num = 97;
// 将int类型转换为char类型
 char ch = static_cast<char>(num);
  • dynamic_cast:运行时类型转换,支持RTTI类型识别,只有转换成RTTI类型或基类,才会成功,否则指针为nullptr,引用的话则抛出异常

RTTI(运行时类型识别)的实现依赖于虚函数表,每个多态类(包含虚函数的类)的虚函数表中都存储了该类的类型信息。当使用RTTI时,编译器会通过这些信息在运行时确定对象的实际类型。

cpp 复制代码
// derive2、derive1继承自base
void showFunc(Base* p)
{
    // 传入的是Derive2才会调用derive02func(),否则调用func()
    Derive2 *pd2 = dynamic_cast<Derive2*>(p);
    if(pd2 != nullptr)
    {
        pd2->derive02func();
    }
    else
    {
        p->func();//动态绑定
    }
}
  • const_cast:用于移除、添加指针或者应用的const属性,但是如果本身指向的就是常量,使用移除const指针修改内容会出错
cpp 复制代码
const int a = 10; // a 是常量
const int* p = &a; // p 是指向常量的指针
int *ptr = const_cast<int *>(p); //虽然ptr修改之后就会出现行为未定义
//如果是int a = 10; ptr才能修改
  • reinterpret_cast:底层、低级别的转换,将变量中的值简单按位重新解释,当然对于明显不合规的转换还是会报错
cpp 复制代码
int x = 42;
int* intPtr = &x;
uintptr_t address = reinterpret_cast<uintptr_t>(intPtr);
std::cout << "Address as integer: " << address << std::endl;

类型别名

1、typedef

保持和c兼容,但是由于不方便阅读,不推荐

cpp 复制代码
typedef int int_ct;
using int_cppt = int;
typedef int(*func) (int, int);
using func_cpp = int(*)(int, int);

int print(int_ct a, int_cppt b) {
	std::cout << a << b << std::endl;
	return a;
}

int main() {
	func f = print;
	func_cpp f1 = print;
	f(1, 2);
	f1(3, 4);
	return 0;
}

2. using

类似于赋值形式,更加直观,且支持模板别名

cpp 复制代码
using IntPtr = int*;
using FuncPtr = void(*)(int,int);

// 简化模板参数。`Vec<int>`等价于`std::vector<int>`
template<typename T> using Vec = std::vector<T>; 

// 类中别名继承
class Parent { 
public: 
    using ValueType = int; 
}; 
class Child : public Parent { 
public: 
    using Parent::ValueType; // 继承别名 
};

auto与decltype

  1. 两者都是类型推导
  2. auto忽略顶层const和引用,decltype甚至会保留左值引用和右值引用
  3. 结合函数尾置返回类型,实现模板编程
  4. c++14起可以省略尾置返回类型,而由auto直接借助return后面表达式来推导,当然此时会省略引用、顶层const,甚至出现歧义,此时就无法省略
cpp 复制代码
template<typename T, typename U>
auto multiply(T t, U u) -> decltype(t * u) {
    // 前面不知道返回类型,所以需要后置返回类型
    // c++14起可以省略-> -> decltype(t * u)
    return t * u;
}
  1. c++14中又引入decltype(auto),实际等价于 decltype(expr),其中 expr 是函数返回表达式或初始化表达式
cpp 复制代码
int x = 1; 
decltype(auto) a = x; // int,等价于decltype(x)
decltype(auto) b = (x); // int&(括号强制为左值表达式), 等价于decltype((x))

1. struct

cpp中struct和class几乎共用,唯一区别在于默认权限不同,但是c中的struct却只能定义数据成员,也不支持继承,但是可以使用函数指针来模拟类

2. 引用

c不存在引用,但是可以使用*&来模拟

枚举enum

c中的枚举定义类似于全局定义了多个宏,枚举变量底层依然是intunsigned int

cpp中普通枚举依然如此,但是增加了enum class,使得枚举内容并不是暴露在全局,且赋值给int必须显式转换

c 复制代码
enum Color { RED, GREEN, BLUE };  //全局中就存在了这三个"宏"
//enum Feeling { EXCITED, BLUE }; // 错误!BLUE重定义
//int RED = 10; // 错误!RED重定义
int color = RED;
cpp 复制代码
enum class Color { RED, GREEN, BLUE };
enum class Feeling { HAPPY, RED, SAD }; // 合法
Color c = Color::RED; // 正确
//int i = Color::RED; // 错误 
int i = static_cast<int>(Color::RED); // 必须显式转换

初始化列表

定义

定义变量时同时提供初始值,提供初始值的方式有:

cpp 复制代码
int a;//默认初始化,未赋值不要直接打印
int b = 2;//拷贝初始化
int c(3);//直接初始化
//列表初始化(统一初始化)
int d{};
int e = {3};
int f{ 2 };

推荐使用列表初始化,这样对于数组也是统一的,而且不允许大类型转小类型,比如使用int a = {2.5};是会报错的,而使用int a = 2.5;就会默认丢失,此时a为2。

相关推荐
半桔11 分钟前
【STL源码剖析】从源码看 vector:底层扩容逻辑与内存复用机制
java·开发语言·c++·容器·stl
洛卡卡了22 分钟前
面试官问限流降级,我项目根本没做过,咋办?
后端·面试·架构
千里镜宵烛41 分钟前
互斥锁与条件变量
linux·开发语言·c++·算法·系统架构
ezl1fe41 分钟前
RAG 每日一技(十四):化繁为简,统揽全局——用LangChain构建高级RAG流程
人工智能·后端·算法
amazingCompass1 小时前
Java 开发必备技能:深入理解与实战 IntelliJ IDEA 中的 VM Options
后端
爱科研的瞌睡虫1 小时前
C++线程中 detach() 和 join() 的区别
java·c++·算法
欧的曼1 小时前
cygwin+php教程(swoole扩展+redis扩展)
开发语言·redis·后端·mysql·nginx·php·swoole
巴拉巴巴巴拉1 小时前
Spring Boot 整合 Thymeleaf
java·spring boot·后端
凤年徐1 小时前
【数据结构与算法】刷题篇——环形链表的约瑟夫问题
c语言·数据结构·c++·算法·链表
用户1512905452202 小时前
Docker部署 Alist
后端