C++基础
const关键字
const含义
被修饰的值不能改变,是只读变量。必须在定义的时候给他赋值(如果是类成员变量也可以使用初始化列表进行初始化)
const与指针
const与指针分别三种:
-
指向常量的指针
-
自身是常量的指针
-
前面两种的组着:指向常量的常指针
指向常量的指针/底层const
指向常量的指针定义了一个指针,这个指针指向一个只读对象,不能通过该指针来改变这个对象的值
特点:强调的是指针所指向的对象不可改变性。
底层const:表示指针所指的对象是一个常量
形式:
const 数据类型 *指针变量 = 变量名;
数据类型 const *指针变量 = 变量名;
示例代码:
int a = 1;
int b = 2;
const int *p = &a;
int const *p = &a;
// 这两种声明式等价的,他们都声明了一个指向常量的指针
*p = 10; //错误,不可更改指向对象的值
*p = &b; //正确,可以指向别的对象
a = 10; //正确,在源对象上修改
自身是常量的指针/顶层const
自身是常量的指针:定义了一个指针,这个指针只能在定义时初始化
特点:强调的是指针的不可改变性
顶层const:顶层const表示指针本身是一个常量
形式:
const 数据类型 *const 指针变量 = 变量名;
示例代码:
int a = 12;
int b = 23;
const int *const p = &a;
*p = 10;//错误,不可更改指向对象的值
p = &b; //错误,不可指向别的对象
a = 10;// 正确,在源对象上修改
记忆方式
-
利用英文从右往左读,并且以to为分解,to之前为描述指针的特性,to之后为描述指针指向的对象特性
// (1) p is a pointer to const char // (1) p 是一个指针指向 const char,即指针所指向的 char 变量的值不可透过指针修改 const char *p; char const *p; //(2)p is a const pointer to char //(2)p 是一个 const 指针指向一个 char,即 p 不可改变指向的地址 char *const p; //(3)p is a const pointer to const char //(3)p 是一个 const 指针指向 const char,即 p 不可改变指向的地址,也不可改变指向的对象的值 const char *const p;
-
如果 const 位于 * 的左侧,则 const 就是用来修饰指针所指向的对象,即指针指向的对象不可透过指针修改
如果 const 位于 * 的右侧,则 const 就是修饰指针本身,即指针本身是常量,不可改变指向的地址
小结
-
当指向的目标特性为const,则指针指向的对象的内容不可透过指针修改。
-
当指针呗加上const特性,则指针不可改变指向的地址
-
当上述两种情况都包含的情况,则指针不可改变指向地址且指针指向的对象的内容不可透过指针修改。
const与引用
只有一种:对常量的引用,没有const reference,因此引用只是对象的别名,引用不是对象,不能用const修饰
对常量的引用(reference to const) -简称常量引用
特点:不能修改他所绑定的对象
形式:
const 数据类型 &引用变量 = 变量名;
示例1:引用机器对应的对象都是常量
const int a = 2;
const int &reference = a; //正确,引用及其对应的都是常量
int &reference2 = &a; //错误,不能让一个非常量引用指向一个常量对象
reference = 10; //错误,常来那个引用不能修改绑定的对象
示例2:引用为常量,其对应的对象不是常量
int a = 2;
const int &reference1 = a;//正确,引用是常量,其对象不是常量
int &reference2 = &a; //正确,引用和对象都不是常量
reference1 =10; //错误,常量引用不能修改所绑定的对象
reference2 = 10; //正确,普通应用可以修改绑定的对象
const与函数
const修饰函数返回值
示例:
const int function1(); //返回一个常数
const int *function2(); //返回一个指向常量的指针变量,使用:
const int *p = funttiond2();
int *const function3(); //返回一个指向变量的常指针,使用:
int *p = function3();
cosnt 修饰函数参数
示例代码:
void function4(const int var); //传递过来的参数在函数内不可变
void function5(const char *var); //参数指针所指内容为常量
void function6(char *const vat); //参数指针为常量
void function7(const int &var); //引用参数在函数内为常量
const与类
总结:
-
常对象成员可以在定义的时候复制或使用初始化列表进行初始化
-
常对象成员函数可以用于对重载函数的区分
-
常对象只能调用常成员函数
示例代码1:常对象成员可以使用初始化列表进行初始化
class A
{
private:
const int a; //常对象成员,只能使用初始化列表进行初始化
public:
//构造函数
A():a(0){}; //初始化列表
A(int x):a(x){}; //初始化列表
//const可用于对重载函数的区分
int getValue(); //普通成员函数
int getValue() const;//常成员函数,不得修改类中的任何数据成员的值
};
void function()
{
//对象
A b; //普通对象,可以调用全部成员函数
const A a; //常对象,只能调用常成员函数
}
示例代码2:常对象成员不能被赋值
class MyClass
{
const int myConstMember;
MyClass();
void setMember();
};
void MyClass::setMember()
{
myConstMenber = 12;//错误,常量不可被修改;
}
int main()
{
MyClass testClass;
return ;
}
示例代码3:常对象成员可以在定义的时候赋值
class MyClass {
public:
const int myConstMember = 4;
};
int main(int argc, char* argv[])
{
MyClass testClass;
cout << "myConstMember:" << testClass.myConstMember << endl;
return 0;
}
const 和 #define 的区别
宏定义 #define | const 常量 |
---|---|
宏定义,相当于字符替换 | 常量声明 |
预处理器处理 | 编译器处理 |
无类型安全检查 | 有类型安全检查 |
不分配内存 | 要分配内存 |
存储在代码段 | 局部常量存放在栈区;全局常量编译期一般放在符号表中,不分配内存;字面值常量,比如字符串,存放在常量区 |
总结
-
修饰变量,说明该变量不可以改变
-
修饰指针
-
指向常量的指针(pointer to const):强调的是指针所指向的对象不可改变性
-
自身是常量的指针(常量指针,const pointer):抢到的是指针的不可改变性
-
前两种结合:指向常量的常指针:既强调指针所指向的对象的不可改变性又香调指针的不可改变性
-
-
修饰引用
- 指向常量的引用(reference to const):不能修改引用绑定的对象,用于形参类型,即避免了拷贝,又避免了函数对值的修改。
-
修饰类成员函数,可用于重载函数的区分,也说明该函数内不能修改成员变量。
-
修饰类成员变量:只能赋值时初始化或使用初始化列表进行初始化。
-
修饰类:只能调用调用类成员函数。
C++内存
堆和栈的区别
-
堆栈空间分配不同。栈由操作系统自动分配释放,存放函数的参数值,局部变量的值等;堆一般由程序员分配释放。
-
堆栈缓存方式不同。栈使用的是一级缓存,他们通常都是被调用时处于存储空间中,调用完毕立即释放;堆则是存放在耳机缓存,速度要慢一些。
-
堆栈数据结构不同。堆类似数组结构;栈类似栈结构,先进后出。
内存管理
内存分配方式
在C++中,内存分成五个区,分别是堆、栈、自由存储区、全局/静态存储区和常量存储区。
-
栈:在执行函数式,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时,这些存储单元自动被释放。
-
堆:就是那些由new分配的内存块,一般一个new就要对应一个delete。
-
自由存储区:就是那些由malloc等分配的内存块,和堆十分相似,不过是用free来结束生命。
-
全局/静态存储区:全局变量和静态变量被分配到同一块内存中。
-
常量存储区:存放常量,不允许修改。
常见的内存错误及其对策
错误:
-
内存分配为成功,却使用了
-
内存分配虽然成功,但未初始化就引用
-
内存分配成功并且已经初始化,但越过了内存便捷
-
忘记释放内存,造成内存泄漏
-
释放了内存却还在继续使用
对策:
-
定义指针时,先初始化为nullptr
-
用malloc或new申请内存之后,应该立即检查指针是否为nullptr,防止指针值为空的内存
-
不要忘记数组和动态内存赋初值。防止将违背初始化的内存作为右值使用
-
避免数字或指针的下标越界,特别要当心发生多1或少1操作
-
动态内存的申请与释放必须配对,防止内存泄漏。
-
用free或delete释放内存后,立即将指针设置为nullptr,防止野指针
内存泄漏及其解决办法:
什么是内存泄漏
简单的说就是申请了一块内存,使用完毕后没有释放掉
-
new和malloc申请资源使用后,没有delete和free释放。
-
子类继承父类,父类析构函数不是虚函数。
-
Windows句柄资源使用后没有释放。
解决:
-
良好的编码习惯,使用内存分配的函数,一旦使用完毕,要记得使用其相应的函数释放掉。
-
将分配的存的指针以链表的形式自行管理,使用完毕之后从链表中删除,程序结束时可检查该链表。
-
智能指针
程序的section
一个程序由代码段、数据段、BSS段组成
-
数据段:存放程序中已初始化的全局变量和静态变量的一块内存区域
-
代码段:存放程序执行代码的内存区域。只读,代码段的头部还会包含一些只读的常数变量
-
BSS段:存放程序中未初始化的全局变量和静态变量
-
可执行程序在运行时又会多出两个区域:
-
堆区:动态申请内存使用,堆冲低地址向高地址增长。
-
栈区:存储局部变量、函数参数值、栈从高地址向低地址增长。是一块连续的空间
-
程序的启动过程
-
操作系统首先创建相应的进程并分配私有的进程空间,然后操作系统的加载器负责吧可执行的文件的数据段和代码段映射到进程的虚拟内存空间中
-
加载器读入可执行程序的导入符号表,更具这些符号表可以查找出该执行程序的所有依赖的动态链接库
-
加载器针对该程序的每一个动态链接库调用LoadLibrary
-
初始化应用程序的全局变量,对全局对象自动调用构造函数
内存对齐
内存对齐应用于三种数据类型中:struct/class/unio
四个原则:
-
数据成员对齐规则:结构(struct)或联合(union)的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的其实位置要从该成员大小或者成员大小的整数倍开始。
-
结构体作为成员:如果一个结构体里有某些结构体成员,则结构体成员要从其内部最宽的基本类型成员的整数倍地址开始存储。
-
收尾工作:结构体的总大小,也就是sizeof的结果,必须是其内部最大成员的最宽基本类型成员的张数被。不足的要补气。
-
sizeof(union),以结构体里面size最大元素为union的size,因为在某一时刻,union只有一个成员真正存储于该地址
extern、static、this、inline、volatile关键字
extern
定义: 关键字extern可以应用于全局变量、函数或模板声明。它指定符号具有external(外部链接),连接器会在当前项目的所有文件中去查找该实体的唯一一次实体。
简单含义: 使用extern关键字,可以让编译器把寻找定义 推迟到链接阶段,而不会在编译阶段出现没有定义的error。
主要作用:
-
使用其他文件可以通过这个声明来使用全局变量或函数
-
引用C语言的函数或变量
注意: C++11非const变量默认为extern
修饰非const全局变量
链接器会在当前项目的所有文件汇总去查找该实体的唯一一次定义
//fileA.cpp
int i = 42; // 声明并定义
//fileB.cpp
extern int i; // 只声明,可以在该文件中使用 fileA.cpp 定义的 i
//fileC.cpp
extern int i; // 只声明,可以在该文件中使用 fileA.cpp 定义的 i
//fileD.cpp
int i = 43; // LNK2005! 'i' already has a definition. 报错:i 已经被定义
extern int i = 43; // LNK2005! 'i' already has a definition. 报错:i 已经被定义
修饰const全局变量
链接器会在当前项目的所有文件中去查找实体的唯一一次定义
//fileA.cpp
extern const int i = 42; // extern const 声明并定义
//fileB.cpp
extern const int i; // 只声明,可以在该文件中使用 fileA.cpp 定义的 i
extern"C"
extern"C"的作用是让C++编译器将extern"C"声明的代码当做C语言代码处理,可以避免C++因符号修饰导致代码不能和C语言库中的符号进行连接的问题
#ifdef __cplusplus
extern "C" {
#endif
void *memset(void *, int, size_t);
#ifdef __cplusplus
}
#endif
注意:被extern"C"修饰的变量和函数式按照C语言的方式编译和连接的,函数重载等是不被支持的
static
作用:修饰存储位置(生命周期)或作用于(可见性)
对于变量来说,static和非static的对比
变量类型 | 存储位置 | 生命周期 | 作用域 | 初始化位置 |
---|---|---|---|---|
局部变量 | 栈区(stack) | 在包含变量的语句块内 | 在包含变量的语句块内 | 在包含变量的语句块内或者不初始化都可以 |
静态局部变量 | 静态区(.bss 或 .data) | 程序的生命周期内 | 在包含变量的语句块内(语句块结束虽然变量仍存在,但不能使用它) | 在 main 函数运行前,有初始值就用初始值初始化它,如果没有初始值,系统用默认值初始化它 |
全局变量 | 静态区(.bss 或 .data) | 程序的生命周期内 | 当一个源程序由多个源文件组成时,则在各个源文件都是有效的 | 在 main 函数运行前,有初始值就用初始值初始化它,如果没有初始值,系统用默认值初始化它 |
静态全局变量 | 静态区(.bss 或 .data) | 程序的生命周期内 | 局限于本源文件内,只能为该源文件内的函数公用 | 在 main 函数运行前,有初始值就用初始值初始化它,如果没有初始值,系统用默认值初始化它 |
类成员变量 | 静态区(.bss 或 .data) | 对象存在期间 | 对应的类的对象 | 类内 |
类静态成员变量 | 静态区(.bss 或 .data) | 程序的生命周期内 | 类的所有对象 | 在类外初始化 |
对于函数来说,static和非static的对比
函数类型 | 存储位置 | 生命周期 | 作用域 |
---|---|---|---|
普通函数 | 代码区 | 程序的生命周期内 | 当一个源程序由多个源文件组成时,则在各个源文件都是有效的 |
静态普通函数 | 代码区 | 程序的生命周期内 | 仅在定义该函数的文件内才能使用 |
类成员函数 | 代码区 | 对象存在期间 | 对应的类的对象还存在时 |
类静态成员函数 | 代码区 | 程序的生命周期内 | 整个类所拥有 |
静态局部变量:
当变量声明为static时,空间将在程序的生命周期内分配。及时多次调用该函数,静态变量的空间也只分配一次,前一次调用中的变量值通过下一次函数调用传递。
静态全局变量:
修饰全局变量时,修改变量的作用于,使其从成功工程课间变成本文件可见
静态对象
类对象为静态,就像变量一样,对象也在声明为static时具有范围,声明周期为程序的生命周期
静态成员函数
我们呗允许使用.来调用静态成员函数,但建议使用雷鸣和范围解析运算符调用静态成员函数
this
-
this指针是一个隐含与每一个非静态成员函数中的特殊指针。它指向调用该成员函数的那个对象。
-
当对一个对象调用成员函数时,编译程序香江对象的地址赋值非this指针,然后调用成员函数,每次成员函数存取数据成员时,痘印是使用了this指针。
-
this指针在被因哈地声明为:ClassName * const this(常指针)
-
this并不是一个常规变量,而是一个右值,所以不能取得this的地址
-
使用场景:
-
为实现对象的链式引用
-
为避免对同一对象进行赋值操作
-
在实现一些数据结构时,如 list
inline
C++内联函数是一种在编译时将函数的定义插入调用点的机制。它们通常用于优化小而频繁调用的函数,以减少函数调用的开销。
内联函数的声明通常放在头文件中,并使用关键字"inline"进行修饰。内联函数的定义通常在头文件的实现部分中,在函数定义前加上关键字"inline"。
例如:
// 在头文件中声明内联函数
inline int add(int a, int b);
// 在实现部分中定义内联函数
inline int add(int a, int b) {
return a + b;
}
当调用内联函数时,编译器会将函数调用处替换为函数体中的代码,以减少函数调用的开销。这样可以提高程序的执行效率。但需要注意的是,内联函数适用于函数体较小且被频繁调用的情况。若函数体过大,仍然会增加代码的冗余和可执行文件的体积。
尽管内联函数可以提高程序的执行效率,但编译器是否真正内联函数的决定权在于编译器自身的优化策略。你可以使用关键字"forceinline"(具体取决于编译器)来要求编译器强制内联函数。
需要注意的是,内联函数在编译时进行替换,而不是运行时。因此,对于动态绑定和虚函数来说,内联函数并不适用。此外,内联函数的定义必须在调用之前可见,因此通常声明和定义都放在头文件中。
少函数调用的开销。
内联函数的声明通常放在头文件中,并使用关键字"inline"进行修饰。内联函数的定义通常在头文件的实现部分中,在函数定义前加上关键字"inline"。
例如:
// 在头文件中声明内联函数
inline int add(int a, int b);
// 在实现部分中定义内联函数
inline int add(int a, int b) {
return a + b;
}
当调用内联函数时,编译器会将函数调用处替换为函数体中的代码,以减少函数调用的开销。这样可以提高程序的执行效率。但需要注意的是,内联函数适用于函数体较小且被频繁调用的情况。若函数体过大,仍然会增加代码的冗余和可执行文件的体积。
尽管内联函数可以提高程序的执行效率,但编译器是否真正内联函数的决定权在于编译器自身的优化策略。你可以使用关键字"forceinline"(具体取决于编译器)来要求编译器强制内联函数。
需要注意的是,内联函数在编译时进行替换,而不是运行时。因此,对于动态绑定和虚函数来说,内联函数并不适用。此外,内联函数的定义必须在调用之前可见,因此通常声明和定义都放在头文件中。