sizeof
如果后面跟类型名,则必须加括号,如sizeof(sockaddr_in)
,告知编译器这是类型
cpp
sizeof int *p; // 歧义:是 (sizeof int) * p,还是 sizeof(int *)
如果后面跟变量名,则都可以sizeof(a)
或 sizeof a
const
普通变量
- 如果在编译时就确定了值,此时普通变量成了常量,编译过程中,把出现常量名字的地方,用常量的值进行替换
cpp
const int a = 10;
int *p = (int *)&a;//此处就必须给a分配内存
*p = 20;
std::cout << a << " " << *p << std::endl;
//10 20,会在编译时就将a变为10
- 宏定义是在预处理的时候直接进行替换,不会存在类型、空间和作用域等问题,但是常量会有空间(可能被优化掉)、类型和作用域
指针或引用
- 如果修饰的是指向的对象,则对象的非静态数据成员(除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;
}
};
成员函数
- 当修饰成员函数时,其实修饰的是传入的this指针 ,因此也是重载,注意普通函数不能使用const修饰
- 不会限制通过引用或指针修改成员变量指向的对象,见上面的代码
cpp
void fun(int a,int b) const{}//方法一
void const fun(int a,int b){} //方法二
// 这两种写法的本质是:void fun (const 类 *this, int a,int b);
mutable
- 修饰非静态成员变量,核心作用是突破
const
成员函数的限制,能在const修饰的成员函数中修改,使用场景在于锁 - 修饰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");
}
函数
函数参数匹配
- 调用 构造函数 进行隐式转换(如果可用)。
- 调用 类型转换运算符 (如
operator int()
)。 - 如果无法转换,则查找下一个候选函数。
尾置返回类型
- auto func(params)
-> ReturnType
{} -
\]()`->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;
函数重载
函数重载需要函数名、作用域相同,而参数不同(类型、数量、顺序)
- 这里参数类型包括了普通类型、引用类型和指针类型之间的不同,如
int a
与int &a
会构成重载- 当const修饰指向的对象时,也会构成重载,如
int& a
与const int& a
会构成重载;- 但是如果只是修饰的变量,则不构成重载,如
int* const a
与int *a
不 构成重载
运算符重载
运算符重载的查找顺序
- 类内重载(成员函数形式) Cpp class MyClass { public: MyClass operator+(const MyClass& other); // 类内重载 + };
- 全局重载(非成员函数形式) Cpp MyClass operator+(const MyClass& a, const MyClass& b); // 全局重载 +
- 默认行为 (如内置类型的
+
) - 如果都不匹配,报错
特殊运算符
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[]
- 操作符new是无法重载的,但是operator new是可以重载的,operator new只是负责分配内存,当使用操作符new时会调用这个函数,然后由编译器调用构造函数再进行隐私转换成对应指针 所以咱们重构operator new等函数,就是为了分配内存,当涉及较多增删时,可以使用内存池,一开始就分配很多个对象,需要时直接取下即可
- 当使用new[]分配自定义类型时,编译器会在最前面增加四个字节,来记录分配了多少个,告知之后调用析构函数多少次,返回的确是这四字节之后的地址,在重载new[]不需要写出
- delete[]释放自定义类型时,会释放前面四个字节,所以在对于自定义类型时,要注意对应关系
- 对于基本类型,由于不需要调用析构函数,所以不会分配之前的四字节,也就可以混用,但是不推荐
- malloc是按字节分配,所以传入是字节数,传出是void*,而new是按类型T分配,返回的就是T*,free传入的是malloc分配的首地址
- 定位new ,在预先分配(malloc)的内存上构造函数:
new(memory) T(arg...);
但是这样之后无法调用delete,而需要手动调用析构函数- 在类中实现重载,为静态方法 ,即使没有添加
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即可
模板
函数模板
函数模板是不进行编译的!!!在函数调用点进行实例化成为模板函数,这个才是被编译器所编译的,连接器要找的也就是这个。所以尽量把函数模板与调用点置于同一个cpp文件或头文件中,否则可能因为调用点没有编译出实例化代码,导致后续链接出错
实例化可以通过
- 显示指定
function<Type>(Type arg1...)
- 实参推演
function(Type arg1...)
,但是参数类型总数要小于模板类型参数- 特例化(不支持部分特例化,可能导致歧义)
cpp
// 特例化
template<>
bool compare<const char*>(const char* a,const char* b){
return strcmp(a,b)>0;
}
- 模板函数compare<Type>(arg...)才是函数符号,所以与compare(arg...)不算真正构成重载,但是编译器依然会将两者优先级进行比较
- 重载决议优先级:普通函数 > 特例化模板 > 通用模板实例
- 非类型参数必须是(int/size_t)整数类型
template<typename T,int SIZE>
,传入时使用常量整型(地址或引用也可以)- 实参推演加上库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;
}
类模板
- 类模板--》实例化--》模板类className<T>
- 在使用类时,注意类名为className<T>,构造、析构函数可以省略后面的<T>,类内部使用类型时也可省略,但是如果在类外部,就必须使用模板类类名
- 成员函数代码只会在调用时才生成(即实例化)
- 可以给参数类型设置默认值
template<typename T = int>
,在调用时可以className<> a
即可- 部分特例化
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); // 函数模板声明
- args为一个参数包,包含0到任意个模板参数,无法直接获得里面的参数
- sizeof...运算符,
sizeof...(args)
统计函数参数数量,Args
则是统计类型参数数量 - 参数包展开,以获取里面的参数
- 递归函数展开,函数包展开函数+递归终止函数,比较麻烦
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
- 在合适上下文中展开,如初始化列表、函数传参等使用
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)...)
// 这里就有模板参数列表、函数列表
强制类型转换
- 类型的作用是赋予内存中的二进制数据(0和1的组合)特定的解释规则和操作方式 。例如,同样的4字节数据,若声明为
int
类型,CPU会将其视为补码形式的整数;若声明为float
类型,则会按IEEE 754标准解析为浮点数。- 赋值从硬件层面看就是内存中的二进制迁移到另一块内存中,而编译器会借助类型判断这样做是否合法(隐式转换),可以人为干预编译器(强制类型转换)
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
- 两者都是类型推导
- auto忽略顶层const和引用,decltype甚至会保留左值引用和右值引用
- 结合函数尾置返回类型,实现模板编程
- 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;
}
- 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中的枚举定义类似于全局定义了多个宏,枚举变量底层依然是int
或unsigned 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。