文章目录
- 前言
- 一、c++基础
- 1.指针和引用
- 指针和引用的区别
- 2.数据类型
- 整型 short int long 和 long long
- 无符号类型
- 3.关键字
- const
- static
- const和static的区别
- define 和 typedef 的区别
- define 和 inline 的区别
- const和define的区别
- new 和 malloc的区别
- constexpr
- volatile
- extern
- 前置++与后置++
- std::atomic
- 待续
前言
总结c++语法、内存等知识。仅供个人学习记录用
一、c++基础
1.指针和引用
指针和引用的区别
指针存放某个对象的地址,其本身就是变量(命了名的对象),本身就有地址,所以可以有指向指针的指针;可变,包括其所指向的地址的改变和其指向的地址中所存放的数据的改变
引用就是变量的别名,从一而终,不可变,必须初始化
- 定义和声明
指针是⼀个变量,其值是另⼀个变量的地址。声明指针时,使用*
符号。
引用是⼀个别名,它是在已存在的变量上创建的。在声明引用时,使用&
符号。 - 使用和操作
指针可以通过解引用操作符 * 来访问指针指向的变量的值,还可以通过地址运算符 & 获取变量的地址。
引用在声明时被初始化,并在整个生命周期中一直引用同一个变量。不需要使用解引用操作符,因为引用本身就是变量的别名 - 空值和空引用
指针可以为空(nullptr)表示不指向任何有效的地址。
引用必须在声明时初始化,并且不能在后续改变引用的绑定对象。因此,没有空引用的概念
不存在指向空值的引用,但是存在指向空值的指针 - 可变性
指针可以改变指针的指向,使其指向不同的内存地址。
引用⼀旦引用被初始化,它将⼀直引用同⼀个对象,不能改变绑定。 - 用途
指针通常用于动态内存分配、数组操作以及函数参数传递。
引用通常用于函数参数传递、操作符重载以及创建别名。
2.数据类型
整型 short int long 和 long long
C++ 整型数据长度标准:
short 至少 16 位
int 至少与 short ⼀样长
long 至少 32 位,且至少与 int ⼀样长
long long 至少 64 位,且至少与 long ⼀样长
在使用8位字节的系统中,1 byte = 8 bit。
很多系统都使用最小长度,short 为 16 位即 2 个字节,long 为 32 位即 4 个字节,long long 为 64 位即 8 个字节,int 的长度较为灵活,⼀般认为 int 的长度为 4 个字节,与 long 等长。
可以通过运算符 sizeof 判断数据类型的长度。例如sizeof (int)
头文件climits定义了符号常量:例如:INT_MAX 表示 int 的最大值,INT_MIN 表示 int 的最小值
无符号类型
即为不存储负数值的整型,可以增大变量能够存储的最大值,数据长度不变。
int 被设置为自然长度,即为计算机处理起来效率最高的长度,所以选择类型时⼀般选用 int 类型。
3.关键字
const
const的作用
const 关键字主要用于指定变量、指针、引用、成员函数等的性质
- 常量变量:声明常量,使变量的值不能被修改。
- 指针和引用:声明指向常量的指针,表示指针所指向的值是常量,不能通过指针修改。声明常量引用,表示引用的值是常量,不能通过引用修改。
- 成员函数:用于声明常量成员函数,表示该函数不会修改对象的成员变量(对于成员变量是非静态的情况)。
- 常量对象:声明对象为常量,使得对象的成员变量不能被修改。
- 常引用参数:声明函数参数为常量引用,表示函数不会修改传入的参数。
- 常量指针参数:声明函数参数为指向常量的指针,表示函数不会通过指针修改传⼊的数据。
常量指针(底层const)
是指定义了一个指针,这个指针指向⼀个只读的对象,不能通过常量指针来改变这个对象的值。常量指针强调的是指针对其所指对象的不可改变性。
特点:靠近变量名。
形式:
(1)const 数据类型 * 指针变量 = 变量名
(2)数据类型 const * 指针变量 = 变量名
int temp = 10;
const int* a = &temp;
int const *a = &temp;
// 更改:
*a = 9; // 错误:只读对象
temp = 9; // 正确
指针常量(顶层const)
指针常量是指定义了一个指针,这个指针的值只能在定义时初始化,其他地方不能改变。指针常量强调的是指针的不可改变性。
特点:靠近变量类型。
形式:数据类型 * const 指针变量 = 变量名
int temp = 10;
int temp1 = 12;
int* const p = &temp;
// 更改:
p = &temp2; // 错误
*p = 9; // 正确
拓展:
顶层const:指针本身是常量;
底层const:指针所指的对象是常量;
左定值,右定向:指的是const在*的左还是右边
const在*左边,表示不能改变指向对象的值;const在*右边,表示不能更换指向的对象
若要修改const修饰的变量的值,需要加上关键字volatile;
若想要修改const成员函数中某些与类状态无关的数据成员,可以使用mutable关键字来修饰这个数据成员;
static
static关键字主要用于控制变量和函数的生命周期、作用域以及访问权限。
实现多个对象之间的数据共享 + 隐藏,并且使用静态成员还不会破坏隐藏原则;
- 静态变量
在函数内部使用 static 关键字修饰的变量称为静态变量。
静态变量在程序的整个生命周期内存在,不会因为离开作用域而被销毁。
静态变量默认初始化为零(对于基本数据类型)。
void exampleFunction() {
static int count = 0; // 静态变量
count++;
cout << "Count: " << count << endl;
}
- 静态函数
在类内部使用 static 关键字修饰的函数是静态函数。
静态函数属于类而不是类的实例,可以通过类名直接调用,而无需创建对象。
静态函数不能直接访问非静态成员变量或非静态成员函数。
class ExampleClass {
public:
static void staticFunction() {
cout << "Static function" << endl;
}
};
- 静态成员变量
在类中使用 static 关键字修饰的成员变量是静态成员变量。
所有类的对象共享同一个静态成员变量的副本。
静态成员变量必须在类外部单独定义,以便为其分配存储空间。
class ExampleClass {
public:
static int staticVar; // 静态成员变量声明
};
// 静态成员变量定义
int ExampleClass::staticVar = 0;
- 静态成员函数
在类中使用 static 关键字修饰的成员函数是静态成员函数。
静态成员函数不能直接访问非静态成员变量或非静态成员函数。
静态成员函数可以通过类名调用,而不需要创建类的实例。
class ExampleClass {
public:
static void staticMethod() {
cout << "Static method" << endl;
}
};
- 静态局部变量
在函数内部使用 static 关键字修饰的局部变量是静态局部变量。
静态局部变量的生命周期延长到整个程序的执行过程,但只在声明它的函数内可见。
void exampleFunction() {
static int localVar = 0; // 静态局部变量
localVar++;
cout << "LocalVar: " << localVar << endl;
}
const和static的区别
define 和 typedef 的区别
define
- 只是简单的字符串替换,没有类型检查
- 是在编译的预处理阶段起作用
- 可以用来防止头文件重复引用
- 不分配内存,给出的是立即数,有多少次使用就进行多少次替换
typedef - 有对应的数据类型,是要进行判断的
- 是在编译、运行的时候起作用
- 在静态存储区中分配空间,在程序运行过程中内存中只有⼀个拷贝
define 和 inline 的区别
define:
定义预编译时处理的宏,只是简单的字符串替换,没有类型检查,不安全。
inline:
inline是先将内联函数编译完成生成了函数体,直接插入被调用的地方,减少了压栈,跳转和返回的操作。没有普通函数调用时的额外开销;
内联函数是一种特殊的函数,会进行类型检查;
对编译器的一种请求,编译器有可能拒绝这种请求;
C++中inline编译限制:
- 不能存在任何形式的循环语句
- 不能存在过多的条件判断语句
- 函数体不能过于庞大
- 内联函数声明必须在调用语句之前
const和define的区别
const用于定义常量;而define用于定义宏,而宏也可以用于定义常量。都用于常量定义时,它们的区别有:
- const生效于编译的阶段;define生效于预处理阶段。
- const定义的常量,在C语言中是存储在内存中、需要额外的内存空间的;define定义的常量,运行时是直接地操作数,并不会存放在内存中。
- const定义的常量是带类型的;define定义的常量不带类型。因此define定义的常量不利于类型检查。
new 和 malloc的区别
1、new内存分配失败时,会抛出bac_alloc异常,它不会返回NULL;malloc分配内存失败时返回NULL。
2、使用new操作符申请内存分配时无须指定内存块的大小,而malloc则需要显式地指出所需内存的尺⼨。
3、opeartor new /operator delete可以被重载,而malloc/free并不允许重载。
4、new/delete会调用对象的构造函数/析构函数以完成对象的构造/析构。而malloc则不会
5、malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符
6、new操作符从自由存储区上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。
constexpr
const 表示“只读”的语义,constexpr 表示“常量”的语义
constexpr 只能定义编译期常量,而const 可以定义编译期常量,也可以定义运行期常量。
你将一个成员函数标记为constexpr,则顺带也将它标记为了const。如果你将⼀个变量标记为constexpr,则同样它是const的。但相反并不成立,一个const的变量或函数,并不是constexpr的。
constexpr变量
复杂系统中很难分辨一个初始值是不是常量表达式,可以将变量声明为constexpr类型,由编译器来验证变量的值是否是一个常量表达式。
必须使用常量初始化:
constexpr int n = 20;
constexpr int m = n + 1;
static constexpr int MOD = 1000000007;
如果constexpr声明中定义了一个指针,constexpr仅对指针有效,和所指对象无关。
constexpr int *p = nullptr; //常量指针 顶层const
const int *q = nullptr; //指向常量的指针, 底层const
int *const q = nullptr; //顶层const
constexpr函数:
constexpr函数是指能用于常量表达式的函数。
函数的返回类型和所有形参类型都是字面值类型,函数体有且只有一条return语句。
constexpr int new() {return 42;}
为了可以在编译过程展开,constexpr函数被隐式转换成了内联函数。
constexpr和内联函数可以在程序中多次定义,一般定义在头文件。
constexpr 构造函数:
构造函数不能说const,但字面值常量类的构造函数可以是constexpr。
constexpr构造函数必须有一个空的函数体,即所有成员变量的初始化都放到初始化列表中。对象调用的成员函数必须使用 constexpr 修饰
constexpr的好处
- 为一些不能修改数据提供保障,写成变量则就有被意外修改的风险。
- 有些场景,编译器可以在编译期对constexpr的代码进行优化,提高效率。
- 相比宏来说,没有额外的开销,但更安全可靠。
volatile
volatile是与const绝对对立的类型修饰符
影响编译器编译的结果,用该关键字声明的变量表示该变量随时可能发生变化,与该变量有关的运算,不要进行编译优化;会从内存中重新装载内容,而不是直接从寄存器拷贝内容。
作用:
指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值,保证对特殊地址的稳定访问
使用场合:
在中断服务程序和cpu相关寄存器的定义
举例说明:
空循环:
for(volatile int i=0; i<100000; i++); // 它会执⾏,不会被优化掉
extern
定义:声明外部变量(在函数或者文件外部定义的全局变量)
前置++与后置++
self &operator++() { //前置++
node = (linktype)((node).next);
return *this;
}
const self operator++(int) { //后置++
self tmp = *this;
++*this;
return tmp;
}
为了区分前后置,重载函数是以参数类型来区分,在调用的时候,编译器默默给int指定为⼀个0
- 为什么后置返回对象,而不是引用
因为后置为了返回旧值创建了⼀个临时对象,在函数结束的时候这个对象就会被销毁,如果返回引用,那么我请问你?你的对象对象都被销毁了,你引用啥呢? - 为什么后置前面也要加const
其实也可以不加,但是为了防止你使⽤i++++,连续两次的调用后置++重载符,为什么呢?
原因:
它与内置类型行为不一致;你无法获得你所期望的结果,因为第一次返回的是旧值,而不是原对象,你调用两次后置++,结果只累加了一次,所以我们必须手动禁止其合法化,就要在前面加上const。 - 处理用户的自定义类型
最好使用前置++,因为他不会创建临时对象,进而不会带来构造和析构而造成的格外开销。
std::atomic
问题:a++ 和 int a = b 在C++中是否是线程安全的?
答案:不是!
a++:
从C/C++语法的级别来看,这是一条语句,应该是原子的;但从编译器得到的汇编指令来看,其实不是原子的。
其一般对应三条指令,首先将变量a对应的内存值搬运到某个寄存器(如eax)中,然后将该寄存器中的值自增1,再将该寄存器中的值搬运回a代表的内存中
mov eax, dword ptr [a] # (1)
inc eax # (2)
mov dword ptr [a], eax # (3)
现在假设a的值是0,有两个线程,每个线程对变量a的值都递增1,预想⼀下,其结果应该是2,可实际运行结构可能是1!是不是很奇怪?
int a = 0;
// 线程1(执⾏过程对应上⽂汇编指令(1)(2)(3))
void thread_func1() {a++;}
// 线程2(执⾏过程对应上⽂汇编指令(4)(5)(6))
void thread_func2() {a++;}
我们预想的结果是线程1和线程2的三条指令各自执行,最终a的值变为2,但是由于操作系统线程调度的不确定性,线程1执行完指令(1)和(2)后,eax寄存器中的值变为1,此时操作系统切换到线程2执行,执行指令(3)(4)(5),此时eax的值变为1;接着操作系统切回线程1继续执⾏,执行指令(6),得到a的最终结果1。
int a = b
从C/C++语法的级别来看,这是条语句应该是原子的;但从编译器得到的汇编指令来看,由于现在计算机CPU架构体系的限制,数据不能直接从内存某处搬运到内存另外一处,必须借助寄存器中转,因此这条语句一般对应两条计算机指令,即将变量b的值搬运到某个寄存器(如eax)中,再从该寄存器搬运到变量a的内存地址
中:
mov eax, dword ptr [b]
mov dword prt [a], eax
既然是两条指令,那么多个线程在执行这两条指令时,某个线程可能会在第一条指令执行完毕后被剥夺CPU时间片,切换到另一个线程而出现不确定的情况。
解决办法
C++11新标准发布后改变了这种困境,新标准提供了对整形变量原子操作的相关库,即std::atomic,这是⼀个模板类型:
template<class T>
struct atomic:
我们可以传⼊具体的整型类型对模板进行实例化,实际上stl库也提供了这些实例化的模板类型
// 初始化1
std::atomic<int> value;
value = 99;
// 初始化2
// 下⾯代码在Linux平台上无法编译通过(指在gcc编译器)
std::atomic<int> value = 99;
// 出错的原因是这⾏代码调⽤的是std::atomic的拷贝构造函数
// ⽽根据C++11语⾔规范,std::atomic的拷贝构造函数使⽤=delete标记禁止编译器⾃动⽣成
// g++在这条规则上遵循了C++11语言规范。
待续