C++原理高性能
文章目录
- 类结构
- 类成员初始值问题
- 对象
- 实例化类的方式:
- new 关键字创建对象和直接定义对象的区别(方式一、方式二)
- 什么情况下使用指针不分配内存实例化类(方式三)
- 全局作用域对象
- 全局对象变量
- 全局对象指针
- 操作符
- 类的析构函数"~"符号
- 类的调用方式"->"符号
- 类的"::"符号
- 重载
- 函数重载
- 运算符重载
- string与char*
- new 的原理
- class 的原理
- map类使用
- enum类使用
- 宏定义使用
- 杂谈
- 对比C语言
- C++比C主要慢在哪里
- 指针写法
- 为什么不建议使用方式一?
- 为什么要使用方式二?
- 数据结构初始值
- 个人的C++命名规则
类结构
比如AnnexBReader.hpp
文件中存在如下类
class AnnexBReader{
public: //后面定义的所有成员都是公开可访问的
AnnexBReader(std::string & filePath);
~AnnexBReader();
int Open();
int Close();
int ReadNalu(Nalu & nalu);
private: //后面定义的所有成员都是私有的访问的
std::string filePath;
FILE* f = nullptr;
bool isEnd = false;
uint8_t* buffer = nullptr;
int bufferLen = 0;
bool CheckStartCode(int & startCodeLen , uint8_t* bufPtr , int bufLen);
int ReadFromFile();
};
class Demo : private AnnexBReader{
int Info(); //没有声明权限则默认为"private:"
}
- 类结构中如果没有声明
public:"
默认都是都是为private:
当存在声明时会结束上一个声明的区域范围并开始声明的权限范围 - 类名冒号后面的是用来定义类的继承,继承方式:
public
、private
和protected
,默认处理是public
注意:如果在头文件里面声明了构造函数和析构函数,那么就一定要对它进行定义,没有定义就会报错!!!
类成员初始值问题
C++11 前不支持类成员初始值
使用 new 运算符分配内存时,可以指定对象的初始值。如果没有指定初始值,那么新分配的内存空间会被默认初始化,也就是说,内存中的每一个字节都会被设置为 0 或者某个默认值。
对象
实例化类的方式:
-
AnnexBReader reader(filePath);
会分配内存、并自动释放 -
AnnexBReader *reader = new AnnexBReader(filePath);
会分配内存、不会自动释放- 需要手动的调用
delete reader
来释放内存
- 需要手动的调用
-
AnnexBReader *reader(filePath);
不会分配内存
new 关键字创建对象和直接定义对象的区别(方式一、方式二)
在于对象的存储位置和生命周期:
-
使用 new 关键字创建类的对象时,对象的存储位置是在堆上。对象的生命周期由程序员显式管理,需要手动释放内存。程序员可以通过指针来操作该对象,并可以在堆上分配任意大小的内存空间。
-
直接定义类的对象时,对象的存储位置是在栈上,即自动内存中。对象的生命周期由其作用域决定,一旦对象的作用域结束,对象就会被自动销毁并释放内存。
一般建议使用
new
的方式来实例对象,除非是比较小的类,因为栈空间的内存是非常小的:
-
在Linux内核中,一般程序运行时栈的大小默认是8MB。但是这个大小可以通过修改内核参数来调整。可以通过设置ulimit或者通过在程序中使用setrlimit函数来限制栈的大小。此外,在Linux内核中,还有一个名为"RLIMIT_STACK"的常量,它定义了一个进程可以使用的栈空间的最大大小。这个值可以通过ulimit或者setrlimit函数来设置或查询。程序运行时栈的大小可以根据具体需要进行调整
-
在Linux程序中,可以使用操作系统提供的
mmap
系统调用来请求操作系统分配更多的栈空间。通过这个系统调用程序可以向操作系统请求更多的栈空间。一般不推荐会导致栈溢出问题
什么情况下使用指针不分配内存实例化类(方式三)
场景:需要在函数间传递类对象时,通常需要使用指针来实例化类对象并传递对象的地址。
因为传递对象的副本会导致对象的复制,而复制对象的开销可能会很大,尤其是在对象较大或对象需要频繁传递的情况下。使用指针来传递类对象的地址,从而避免对象复制的开销。
全局作用域对象
全局对象变量
#include <iostream>
MyClass gMyObj(0); // 声明并初始化全局变量 myObj
int main() {
int myX = 42;
gMyObj = MyClass(myX); // 初始化 myObj
gMyObj.printX();
return 0;
}
这个方式不太行,使用全局变量会增加代码的复杂度,并且可能会导致程序出现副作用。因此,如果可能的话,应该尽量避免使用全局变量
全局对象指针
#include <iostream>
MyClass *gMyObj; // 声明并初始化全局指针 myObj
int main() {
int myX = 42;
MyClass myobj(myX); // 初始化 myObj
gMyObj = &myobj; //这里会有警告: Address of local variable may escape the function
gMyObj->printX(); //使用指针方式调用函数
return 0;
}
这个警告是因为:变量 myobj
是在 main
函数中定义的,它的生命周期只存在于该函数内。因为把它的地址 &myobj
赋值给了全局指针 gMyObj
,这意味着 gMyObj
现在指向了一个局部变量。当 main
函数结束时,局部变量 myobj
将被销毁,gMyObj
指向的地址也将变得无效。因此,使用 gMyObj
来访问已经被销毁的对象会导致未定义的行为。
为了解决这个问题,你可以将 myobj
分配在堆上,而不是在栈上。这样,它的生命周期将独立于 main
函数,直到你显式地释放它。你可以使用 new
运算符来分配 MyClass
对象,例如
MyClass *gMyObj;
int main() {
int myX = 42;
MyClass *myobj = new MyClass(myX); // 用 new 分配对象
gMyObj = myobj;
gMyObj->printX();
delete myobj; // 显式释放对象
return 0;
}
操作符
类的析构函数"~"符号
类的析构函数:
- 角色:类的一个成员函数
- 符号:函数名是在类名前加上
~
,无参数且无返回值 - 一个类只能有且有一个析构函数,如果没有显式的定义,系统会生成一个缺省的析构函数
- 执行与构造函数相反的操作.比如:释放对象使用的资源,并销毁非static成员
- 每有一次构造函数的调用就会有一次析构函数的调用
注意:
- 如果是声明指针则不会调用构造函数、析构函数
- 使用new创建分配的时候
AnnexBReader *reader = new AnnexBReader(filePath);
不会调用析构函数、会调用构造函数 。所以需要手动调用delete reader
class AnnexBReader{
AnnexBReader::AnnexBReader(std::string & _filePath){
filePath = _filePath;//赋值成员变量
}
AnnexBReader::~AnnexBReader(){ //析构函数
Close(); //调用定义的function
}
};
类的调用方式"->"符号
class AnnexBReader{
public:
AnnexBReader(std::string & filePath);
~AnnexBReader();
int Open();
int Close();
int ReadNalu(Nalu & nalu);
private:
std::string filePath;
FILE* f = nullptr;
bool isEnd = false;
uint8_t* buffer = nullptr;
int bufferLen = 0;
};
AnnexBReader reader(filePath);
reader.Open(); //call函数
AnnexBReader *p_reader = &reader; //指针
p_reader->Open(); //call函数
这里需要区别于.
符号比如:
- 当类初始化为对象时即通过
.
来访问类成员 - 当类初始化为对象指针时即通过
->
来访问类成员
类的"::"符号
在源代码annexBReader.cpp
文件中
#include "annexBReader.hpp"
int AnnexBReader::Open(){ //表示Open()函数是类AnnexBReader的成员函数
f = fopen(filePath.c_str(),"rb");
if(f == nullptr){
return -1;
}
return 0;
}
::
表示域操作符.当类声明了一个成员函数int Open(),但没有在类的声明里给出定义,那么在类外定义时,就要写成int AnnexBReader::Open()来表示函数是类的成员函数
重载
函数重载
声明是指一个与之前已经在该作用域内声明过的函数或方法具有相同名称的声明,但是它们的参数列表和定义(实现)不相同。这个很好理解
原理:编译器通过把您所使用的参数类型与定义中的参数类型进行比较,决定选用最合适的定义。
运算符重载
C++中存在大量运算符比如:+=,sizeof ,->,Cast
-
重载的运算符是带有特殊名称的函数
-
函数名是由关键字
operator
和其后要重载的运算符符号构成的 -
与其他函数一样,重载运算符有一个返回类型和一个参数列表。
定义为类成员函数形式如下:
Box operator+(const Box&); //可以理解为一种特殊函数
//使用"operator关键字"来声明运算符“+”重载
//它的参数值就是 "Box对象指针"
//它的返回值就是 "Box对象"
上面的声明”加法“运算符重载的作用就是:
用于把两个 Box 对象相加,返回最终的 Box 对象
定义为类非成员函数形式如下:
Box operator+(const Box&, const Box&);
需要为每次操作传递两个参数
代码实例:
#include<iostream>
class Box{
public:
//就是重载类的“+”符号,在类的作用域中使用“+”重载成以下规则
//这里实现了一个将它们的成员变量进行相加功能
Box operator+(const Box &_b){
Box box;
box.name = this->name + _b.name;
box.age = this->age + _b.age;
return box;
}
public:
Box(){}
Box(int _name,int _age){
name= _age;
age= _age;
}
void print(){
printf("name:%d,age:%d\n",name,age);
}
private:
int name = 0;
int age = 0;
};
int main(){
Box box1(1,1);
Box box2(2,2);
Box box3 = box1 + box2; //重载符号“+” 后面的一个变量就是 参数
//使用类重载操作符,从而调用到上面的重载操作符函数
box3.print();
//输出结果如下:
//name:3,age:3
return 0;
}
std::string类的两个string进行拼接就是类似的实现方式:在堆上动态分配一块新的内存,并将两个字符串拷贝到这个内存中,所以它是会影响效率的,大量使用的情况下使用
append
函数来拼接,java的java.lang.String
也是这个实现只不过是每次都会创建第三个java.lang.String
对象来存储拼接后的字符串,由于C++操作符可重载的特性那么可以自己在面对不同作用域的情况下对字符拼接进行重载,具体使用->[c++string字符串拼接性能](c++string字符串拼接性能 TODO)
c++中不可重载的操作符
下面是不可重载的运算符列表:
符号 | 解释 |
---|---|
. | 成员访问运算符 |
.*, ->* | 成员指针访问运算符 |
:: | 域运算符 |
sizeof | 长度运算符 |
?: | 条件运算符 |
# | 预处理符号 |
string与char*
string原理:
std::string
变量的本质是一个对象,类型为string,有一个char型指针的成员变量_M_p
,它指向其管理字符串的数组,作为一个类对外提供使用,具体如下
- 内部维护了一个指针和一个整数,指针指向存储字符串的字符数组,整数表示该字符串的长度(即不包括 null 结尾符)。
- 对象被创建时,它会自动分配一段内存来存储字符串,并将长度设置为 0。此时,指针指向的字符数组的长度为 1(即只有一个 null 结尾符)。
- 对象中添加字符时,它会先检查是否有足够的内存来存储新的字符,如果没有,则会自动重新分配一块更大的内存来存储新的字符串,并将原有的字符串拷贝到新的内存中,最后添加新的字符。
可以得知std::string
主要是使用分配内存的形式(传送门👆->[#new 的原理](#new 的原理))来存储字符串,然后封装了一系列的对内存块的字符串进行操作的函数并以对象的形式对外开放使用
string如何动态管理字符串:
- 当字符串长度小于某个特定阈值时,std::string 会使用内部缓存(GCC 实现中 std::string 的内部缓存大小是 15 个字符)来存储字符串。该缓存是在 std::string 类型中预先分配的一块固定大小的内存,以避免频繁的内存分配和释放。
- 当字符串长度大于阈值时,std::string 会使用 new 操作符来申请一块新的内存,然后将原有字符串拷贝到新的内存中,并释放原有内存。
- 如果字符串长度增加导致内存不够用时,std::string 会重新申请一块更大的内存,并将原有字符串拷贝到新的内存中。
- 当 std::string 对象被销毁时,它会使用 delete 操作符来释放其内部使用的内存。
不同点:
- 内存管理:char* 保存的字符串是存入内存地址的,是固定长度的,一般开发中都会定义一个char* buf 的后面紧跟着定义一个int length 变量来手机记录指针指向的内存长度,每次都要手动改变非常麻烦。 std::string 类型的字符串是由 C++ 标准库自动管理内存的
- 可变性:使用 char * 定义的字符串是一个指针,指向的是一段固定的内存空间,不能随意改变其长度。而 std::string 类型的字符串可以动态调整长度,并且提供了一系列的成员函数来操作字符串
- 传参方式:使用 char * 定义的字符串通常通过指针传递,而 std::string 类型的字符串可以像其他类型一样通过值传递或引用传递,使得函数参数更加简单和易于理解。
c_str()函数:
可以通过类成员函数std::string::c_str()
获取到这个char型数组。其实现原理如下:
-
字符串对象中维护着一个内部缓冲区,用于存储字符串的字符序列和一个空字符 ‘\0’。当调用 c_str() 函数时,该函数会返回一个指向该内部缓冲区的指针,并且该指针指向的字符序列以 null 结尾。
-
c_str() 函数返回的指针是一个指向常量的指针(const char*),不能通过该指针修改字符串的内容,因为内部缓冲区可能被其他对象共享。
-
由于 c_str() 函数只是返回一个指针,并不会对字符串对象进行任何修改,所以该函数是非常高效的,不会对程序的性能产生明显的影响。
new 的原理
new是C++中的关键字,也叫操作符(和if,else之类的一样)
new 操作符的底层实现通常是通过调用 C++ 标准库中的 operator new 函数来完成的。
operator new 函数的作用是申请一块指定大小的内存,并返回指向该内存块的指针。
其实new是用面向对象的思想封装了动态分配内存,其中实现原理和 alloc 几乎相似(传送门👆->[glibc-malloc申请堆空间流程分析][https://blog.csdn.net/csdn546229768/article/details/129583431]),或者简单粗暴的理解就是new通过类封装了 alloc 和 mmap ,从而实现堆块的使用作用域,以及不需要用户申请堆块时手动判断堆块大小
在C++标准库中很多API都是基于new
来实现上层建筑的
重载new
class MyClass {
public:
void* operator new(size_t size) {
void* p = malloc(size); // 调用标准库函数 malloc 进行内存分配
if (!p) { // 内存分配失败时抛出异常
throw std::bad_alloc();
}
return p;
}
// 必须也要重载 operator delete
void operator delete(void* p) {
free(p); // 调用标准库函数 free 进行内存释放
}
// 其他类成员函数和数据成员的定义
};
class 的原理
C++ 中的类是一种自定义数据类型,它将数据和对这些数据的操作封装在一起:
-
成员变量和成员函数的存储
- 类的成员变量和成员函数都会被存储在内存中,但是它们的存储方式有所不同。成员变量的存储通常是按照声明的顺序在内存中连续存储的,这样可以方便地计算出每个成员变量的地址。而成员函数的存储通常是在程序代码段中,每个成员函数的实现都会被编译为一段独立的代码,这些代码会在程序运行时被载入到内存中。
-
类的构造函数和析构函数
-
类的构造函数和析构函数是两个特殊的成员函数。当创建一个类的对象时,构造函数会被调用,用于初始化对象的成员变量;而在对象被销毁时,析构函数会被调用,用于清理对象的资源。
-
构造函数和析构函数的实现都需要考虑类的继承关系和访问权限等因素。例如,如果一个类派生自另一个类,那么在创建派生类的对象时,基类的构造函数也需要被调用,以保证基类的成员变量正确地初始化。
-
-
对象的内存分配和释放
-
在 C++ 中,对象通常是动态分配的,即通过 new 操作符分配一块内存,并在其中创建对象。当对象不再被需要时,可以通过 delete 操作符释放内存,并调用对象的析构函数清理资源。
-
对象的内存分配和释放需要考虑许多因素,例如内存的对齐、对象的大小等。C++ 中还提供了一些机制,例如对象池、智能指针等,用于更加灵活地管理对象的内存分配和释放。
-
类的对象如何寻找自己的成员:
可以通过指针或引用来访问成员变量和成员函数。对象的成员变量和成员函数的地址在编译时就已经确定了,因此在运行时可以直接通过偏移量来访问它们:
-
每个成员变量和成员函数都有一个固定的偏移量,表示它在对象内存布局中的位置。
- 这个偏移量在编译时就已经计算出来,并存储在类的虚函数表(vtable)和虚函数表指针(vptr)中。
-
虚函数表是一个数组,存储了类中所有虚函数的地址。
- 每个类对象都会有一个虚函数表指针,指向它所属的类的虚函数表。当调用一个虚函数时,程序会根据对象的虚函数表指针和虚函数的偏移量来确定要调用的函数地址。
-
类的成员变量的地址可以通过指针算术运算来访问。
- 例如,对于一个类的对象 obj 和它的成员变量 x,可以通过
&obj + offsetof(Class, x)
来获取 x 的地址。offsetof
是一个宏定义(这个运算并不会明显的影响C++性能,这个耗时几乎可以忽略不计)。
- 例如,对于一个类的对象 obj 和它的成员变量 x,可以通过
通过将成员固定偏移量存储在虚函数表和虚函数表指针中。这种机制可以保证类的对象在运行时高效地访问自己的成员变量和成员函数(虚函数是C++一个很重要的模块,同时也是消耗C++性能的一个大原因,传送门👆->#C++比C主要慢在哪里)。
map类使用
#include <map>
std::map<std::string,int> map_profile_idc{
{"PROFILE_BASELINE", 66},
{"PROFILE_MAIN", 77},
{"PROFILE_EXTENDED", 88},
{"PROFILE_HIGH", 100},
{"PROFILE_HIGH_10", 110},
{"PROFILE_HIGH_422", 122},
{"PROFILE_HIGH_444_PREDICTIVE", 244},
{"PROFILE_HIGH_444_INTRA", 44}
};
//方式一
for (auto it : map_profile_idc) {
if(profile_idc == it.second){
printf("type:%s,value:%u\n", it.first.c_str(),it.second);
}
}
//方式二
for (auto &[key,val] : map_profile_idc) {
if(profile_idc == it.val){
printf("type:%s,value:%u\n", it.key.c_str(),it.val);
}
}
- first : 表示map集合的Key键
- second : 表示map集合的Value值
enum类使用
namespace PROFILE_IDC{
enum Type{
PROFILE_BASELINE = 66,
PROFILE_MAIN = 77,
PROFILE_EXTENDED = 88,
PROFILE_HIGH = 100,
PROFILE_HIGH_10 = 110,
PROFILE_HIGH_422 = 122,
PROFILE_HIGH_444_PREDICTIVE = 244,
PROFILE_HIGH_10_INTRA = 100,
PROFILE_HIGH_444_INTRA = 44
};
static const Type All[] = {PROFILE_BASELINE,PROFILE_MAIN,PROFILE_EXTENDED,PROFILE_HIGH,PROFILE_HIGH_10,PROFILE_HIGH_422,PROFILE_HIGH_444_PREDICTIVE,PROFILE_HIGH_10_INTRA,PROFILE_HIGH_444_INTRA};
}
for (auto it : PROFILE_IDC::All) {
printf("type:%s\n",it);
}
宏定义使用
定义如下:
#include <stdio.h>
#define N 178257920 // 定义一个常量 170M
#define SEED 0x1234
void info();
#if defined(NORMAL)
void info() {
printf("NORMAL\n");
}
#elif defined(USE_SSE)
void info() {
printf("USE_SSE\n");
}
#elif defined(USE_AVX)
void info() {
printf("USE_AVX\n");
}
#endif
int main(){
info();
return 0;
}
使用如下:
$ clang main.c -D NORMAL && ./a.out [~/D/demo]
NORMAL
$ clang main.c -D USE_SSE && ./a.out [~/D/demo]
USE_SSE
$ clang main.c -D USE_AVX && ./a.out [~/D/demo]
USE_AVX
杂谈
对比C语言
其实经常看大项目的源码来说,用C写的项目和用C++写的项目是完全不一样的,因为面向对象的思想优越性使程序员习惯性的使用该思维来编写代码,而C本身因为速度的优势性被定位到了面向过程的语言,但是因为struct
结构体和函数指针的可定制性较高那么就可以简单的将它们结合起来实现一个“类结构体”,将结构体作为"类"来使用这样就可以很容易的理解自己代码的结构了(FFmpeg就是这样
)
在我看来C的项目是比C++的项目要难读懂一点的,需要自己去梳理各种功能的”结构体类“,C项目对程序员的代码编写功底要求比较强,简单点说C比C++难
其实C++并不比C慢太多,而且在C的基础上实现了面向对象思想、大量的语法糖、自动化处理机制、可定制化、扩展性等等,但是还是会有一些大项目为了追求极致的性能依旧会使用C来架构项目,毕竟操作系统都是C写的。。
还有一个重要的原因就是C的兼容性,C可以在非常多的平台无缝兼容,比如嵌入式芯片
C++比C主要慢在哪里
-
虚函数是 C++ 中的一个重要特性,它使得对象的成员函数调用可以动态绑定。
- 为了实现虚函数,每个对象都需要存储一个指向虚函数表的指针,而虚函数表本身也需要额外的内存空间。
- 这些额外的内存开销会导致 C++ 的对象比 C 的结构体更大,从而降低了访问对象的速度。
-
运行时类型信息(RTTI)和异常处理机制。
- RTTI 使得程序可以在运行时获取对象的类型信息,这需要在对象中存储额外的类型信息。
- 异常处理机制使得程序可以在出现错误时跳转到异常处理代码,这会带来额外的开销。
指针写法
方式一:int* a
,方式二:int *a
为什么不建议使用方式一?
方式一其实更好理解,直观的看出来a为一个int*
指针,这样的场景有很多,比如常见的:
char *buf = (char*)malloc(0x100);
这里就可以很直观的理解将malloc(0x100)强制转换为一个char*
的指针。但是呢,如果使用多个定义编译器会识别为如下:
int* a, b; // 将a声明为指向int类型的指针,将b声明为int类型
所以方式一的书写方式并不正确。但是理解还是要以int*
的方式去理解,
为什么要使用方式二?
-
为了按照编译器的规则编写代码
-
为了方便阅读源码,比如下面glibc.so的这段源代码:
static void
__malloc_assert (const char *assertion, const char *file, unsigned int line,const char *function)
{
(void) __fxprintf (NULL, "%s%s%s:%u: %s%sAssertion `%s' failed.\n",
__progname, __progname[0] ? ": " : "",
file, line,
function ? function : "", function ? ": " : "",
assertion);
fflush (stderr);
abort ();
}
- 这种命令风格被
LLVM
采样(LLVM就是用C++开发的) - 为了方便阅读教科书上面的代码块,都是指针靠变量名一侧
- 指针变量是一种特殊的变量,它存储了一个地址。在定义指针变量时指定该变量所指向的数据类型,并在变量名前加上一个*号来表示该变量是一个指针变量。因此,*号应该被视为指针类型修饰符的一部分,而不是数据类型的一部分
当然如果非要使用方式一的命令风格,一个比较权威的解释–C++之父回应如下:
-
“典型的C程序员”写“
int *p;
”并解释它“*p是强调语法的int”,并可能指向C(和C++)声明语法来争论样式的正确性。事实上,*与语法中的p名称绑定。 -
“典型的C++程序员”写“
int* p;
”,并解释“p是int的指针”强调类型。事实上,p的类型是int*。我显然更喜欢这种强调,并认为这对很好地使用C++更高级的部分很重要。
数据结构初始值
每次代码没有定义初始值Leader就说这块代码有问题。。。。
那么就来说说为什么要给变量、指针定义初始值:
-
未定义行为是指在程序中使用一个未初始化的变量时,程序的行为是不确定的,比如函数的栈空间频繁的扩张收缩,在后几次中会导致未定以局部变量引用到之前栈的数据,而局部逻辑代码并没有太严谨的检查局部变量的状态,从而可能会出现意料之外的结果。
-
如果给局部变量设置一个初始值,可以确保它在使用之前被初始化,避免未定义行为。
-
另外,给局部变量设置初始值可以提高程序的可读性和可维护性。当其他程序员或自己再次阅读代码时,能够更容易地理解变量的作用和初值,避免混淆和错误
常见的初始值:
-
int *pointer = nullptr;
: 指针一般是nullptr -
int count = 0;
-
int ret = -1;
: ret一般用来接受函数执行返回值。在C\C++中一般函数返回值为0则表示函数执行成功,返回值为负值则表示失败,定义整型为-1就可以确保ret变量准确的接受到了函数返回值
-
float x = 0.0f;
-
double x = 0.0f;
-
char *buf = nullptr
-
char buf[0x100] = {0}
-
std::string name = "";
: 因为string在创建 std::string 对象时,它会自动分配一个足够容纳字符串的内存空间,并将字符串的内容复制到这个内存空间中。
std::string 不能被初始化为 nullptr,因为 nullptr 表示空指针,它不是指向字符数组的指针
一般初始化为nullptr会报错:terminate called after throwing an instance of ‘std::logic_error’ what(): basic_string: construction from null is not valid
个人的C++命名规则
- 变量名、类成员变量: 使用小写字母开头,单词之间使用驼峰式命名法(例如:myVariable)。
- 函数名: 使用小写字母开头,单词之间应该使用驼峰式命名法(例如:myFunction)。
之前为了区分类函数和普通函数我会使用首字母大写来命令类函数,比如MyClass::Create() 有的C++源代码中就有这样的命名,比如(LLVM)
但是我觉得不太符合我的习惯,比如一个数据类
class User{private long id;}
这时候我要设置id以及获取id私有属性,那么对应的函数就应该是void setId(long id)
、long getId()
,感觉SetId、GetId
看起来怪怪的,为了统一所以还是将类函数也使用首字母小写。
- 类、枚举类型: 首字母大写。应该使用驼峰式命名法(例如:MyClass)。
- 宏定义、常量名 应该使用大写字母和下划线组合的命名方式(例如:MY_MACRO)。
- 类的私有成员变量应该在名称前加上“_”前缀(例如:_variable)。
- 虚函数以do开头
- 回掉函数以on开头
实例:
- 类名:Demo
- public : name
- private : _name
- 函数 : getInfo()
- 指针 : int *a //*靠变量名这边
- 常量 : VIDEO_SIZE
- 文件名 : main.cpp \ demo.cpp \ read_file.cpp