文章目录
- sizeof小结
- strlen和sizeof的区别
- new和delete
- new和malloc如何判断是否申请到内存
- new实现原理
- delete实现原理,delete和delete[]的区别
- new和malloc的区别,delete和free搭配使用
- C和C++ struct的区别
- struct和union的区别
- class和struct的异同
- volatile 关键字
- volatile的作用?是否具有原子性,对编译器有什么影响?
- 什么情况下一定要用volatile,能否和const一起使用?
- extern C的作用
- memmove和memcpy的区别以及处理内存重叠问题
- strcpy函数
- strcpy和memcpy的区别
- 实现atoi()函数
- extern关键字
sizeof小结
sizeof
计算的是在栈中分配的内存大小。
sizeof
不计算static
变量的字节。- 32位系统的指针大小是4字节,64位系统的指针是8字节,不需要管指针类型。
char
占1个字节,int
占4个字节,double
占8个字节,string
占4个字节。一个空类占1个字节,单一继承的空类占1个字节,虚继承涉及到虚指针所以占4字节。- 对函数使用
sizeof
,在编译阶段会被函数的返回值的类型代替。
strlen和sizeof的区别
strlen
是头文件<cstring>
中的函数,sizeof
是C++中的运算符。strlen
本身是库函数,因此在程序运行过程中,计算长度;而sizeof
在编译时,计算长度。strlen
测量的是字符串的实际长度,以‘\0’
结束,'\0'
不计入长度。而sizeof
测量的是字符数组的分配大小。sizeof
是运算符,可以以类型,函数来作为参数,strlen
是函数,只能以char*
做参数,而且,要想得到正确的结果,必须包含'\0'
。sizeof
不能计算动态分配空间的大小。
strlen
源代码:
size_t strlen(const char* str)
{
size_t length = 0;
while(*str++)
++length;
return length;
}
sizeof
和strlen
的具体使用
char s[] = "0123456789";
sizeof(s); //结果11,计算到'\0'位置,因此是10+1
strlen(s); //结果10
new和delete
C++语言定义了两个运算符来分配和释放动态内存。运算符new
分配内存,delete
释放new
分配的内存。
自己直接管理内存的类与使用智能指针的类不同,它们不能依赖类对象拷贝,赋值和销毁操作的任何默认定义。
使用new动态分配和初始化对象
在自由空间分配的内存是无名的,因此new
无法为其分配的对象命名,而是返回一个指向该对象的指针。
int* pi = new int; //pi指向一个动态分配的,未初始化的无名对象
此new
表达式在自由空间构造一个int
型对象,并返回指向该对象的指针。
默认情况下,动态分配的对象是默认初始化的,这意味着内置类型或组合类型的对象的值将是未定义的,而类类型对象将用默认构造函数进行初始化。
string* ps = new string; //初始化为空的string
int* pi = new int; //pi指向一个未初始化的int
可以使用直接初始化的方式来初始化一个动态分配的对象。也可以使用传统的构造方式(使用圆括号),在新标准下,也可以使用列表初始化(使用花括号):
int* pi = new int(1024); //pi指向的对象的值为1024
string* ps = new string(10, '9'); //*ps为‘9999999999’
vector<int>* pv = new vector<int>{0,1,2,3,4,5,6,7,8,9};
也可以对动态分配的对象进行值初始化,只需要在类型名之后跟一对空括号即可。
string* ps1 = new string; //默认初始化为空string
string* ps = new string(); //值初始化为空string
int* pi1 = new int; //默认初始化,*pi1的值未定义
int* pi2 = new int(); //值初始化为0
对于定义了自己的构造函数的类类型来说,要求值初始化是没有意义的;不管采用什么形式,对象都会通过默认构造函数初始化。但对于内置类型,两种形式的差别就很大了;值初始化的内置类型对象有着良好定义的值,而默认初始化的内置类型对象的值则是未定义的。
使用delete释放动态内存
通过delete
表达式接受一个指针,指向我们想要释放的对象。
delete p; //p必须指向一个动态分配的对象或者是一个空指针
delete
表达式执行两个动作:销毁给定的指针指向的对象;释放对应的内存。
我们传递给delete
的指针必须指向动态分配的内存,或者是一个空指针。释放一块并非new
分配的内存,或者将相同的指针值释放多次,其行为是未定义的。
使用new和delete管理动态内存存在三个常见问题:
- 忘记
delete
内存。忘记释放动态内存会导致“内存泄漏”问题,因为这种内存永远不可能被归还给自由空间了。查找内存泄漏问题是非常困难的,因为通常应用程序运行很长时间后,真正耗尽内存时,才能能检测到这种错误。 - 使用已经释放掉的对象。通过在释放内存后将指针置为空,有时可以检测到这种错误。
- 同一块内存释放两次。当有两个指针指向相同的动态内存分配对象时,可能发生这种错误。如果对其中一个指针进行了
delete
操作,对象的内存就被归还给自由空间了。如果随后又delete
第二个指针,自由空间就可能被破坏。
new和malloc如何判断是否申请到内存
malloc
:成功申请到内存,返回指向该内存的指针;分配失败,返回NULL
指针。new
:内存分配成功,返回该对象类型的指针;分配失败,抛出bad_alloc
异常。
new实现原理
new
的实现过程:首先调用名为operator new
的标准库函数,分配足够大的原始内存,保存指定类型的一个对象;接下来运行该类型的构造函数,指定初始化构造对象,最后返回指向新分配并构造后的对象的指针。
delete实现原理,delete和delete[]的区别
delete
的实现原理:
- 首先执行该对象所属类的析构函数;
- 进而通过调用
operator delete()
的标准库函数来释放所占的内存空间;
delete
和delete[]
的区别:
delete
用来释放单个对象所占的空间,只会调用一次析构函数;delete[]
用来释放数组空间,会对数组中的每个成员都调用一次析构函数;
new和malloc的区别,delete和free搭配使用
在使用的时候new
,delete
搭配使用,malloc
,free
搭配使用。
-
malloc,free
是库函数,而new,delete
是关键字。new
申请空间时,无需指定分配空间的大小,编译器会根据类型自行计算;malloc
在申请空间时,需要确定所申请空间的大小。 -
new
申请空间时,返回的类型是对象的指针类型,无需强制类型转换,是类型安全的操作符;malloc
申请空间时,返回的是void*
类型,需要进行强制类型的转换,转换为对象类型的指针。 -
new
分配失败时,会抛出bad_malloc
异常,malloc
分配失败返回空指针。 -
对于自定义的类型,
new
首先调用operator new()
函数申请空间(底层通过malloc实现),然后调用构造函数进行初始化,最后返回自定义类型的指针;delete
首先调用析构函数,然后调用operator delete()
释放空间(底层通过free
实现)。malloc,free
无法进行自定义类型对象的构造和析构。 -
new
操作符从自由存储区上为对象动态分配内存,而malloc
函数从堆上分配内存(自由存储区不等于堆)。
C和C++ struct的区别
- C语言中,
struct
是用户自定义数据类型;在C++语言中,struct
是抽象数据类型,支持成员函数的定义; - C语言中
struct
没有访问权限的设置,是一些变量的集合体,不能定义成员函数;C++中struct
可以和类一样,有访问权限,并可以定义成员函数; - C语言中,
struct
定义的自定义数据类型,在定义该类型的变量时,需要加上struct
关键字,例如struct A var
定义A
类型的变量;而在C++中,不用加该关键字,例如:A var
;
struct和union的区别
说明:union
是联合体,struct
是结构体。
区别:
- 联合体和结构体都是由若干个数据类型不同的数据成员组成。使用时,联合体只有一个有效的成员;而结构体所有的成员都有效。
- 对联合体的不同成员赋值,将会覆盖其他成员的值,而对于结构体的不同成员赋值时,相互不影响。
- 联合体的大小为其内部所有变量的最大值,按照最大类型的倍数进行分配大小;结构体分配内存的大小遵循内存对齐原则。
#include <iostream>
using namespace std;
typedef union{
char c[10];
char cc1; //char 1 字节,按该类型的配属分配大小
}u11;
typedef union{
char c[10];
int i; //int 4字节,按该类型的倍数分配大小
}u22;
typedef union{
char c[10];
double d; //double 8字节,按该类型的倍数分配大小
}u33;
typedef struct s1{
char c; //1 字节
double d; //1(char) + 7(内存对齐) + 8(double) = 16字节
}s11;
typedef struct s2{
char c; //1字节
char cc; //1(char) + 1(char) = 2字节
double d; //2 + 6(内存对齐) + 8(double) = 16字节
}s22;
typedef struct s3{
char c; //1字节
double d; //1(char) + 7(内存对齐) + 8(double) = 16字节
char cc; //16 + 1(char) + 7(内存对齐) = 24字节
}s33;
int main(){
cout << sizeof(u11) << endl; //10
cout << sizeof(u22) << endl; //12
cout << sizeof(u33) << endl; //16
cout << sizeof(s11) << endl; //16
cout << sizeof(s22) << endl; //16
cout << sizeof(s33) << endl; //24
}
class和struct的异同
-
struct
和class
都可以自定义数据类型,也支持继承操作。 -
struct
中默认的访问级别是public
,默认的继承级别也是public
;class
中默认的访问级别是private
,默认的继承级别也是private
。 -
当
class
继承struct
或者struct
继承class
时,默认的继承级别取决于class
或struct
本身,class
(private
继承),struct
(public
继承),即取决于派生类的默认继承级别。 -
class
可用于定义模板参数,struct
不能用于定义模板参数。
struct A {};
class B : A {}; //private继承
struct C : B {}; //public继承
volatile 关键字
简单介绍
编译器将C代码转换为机器代码,以便可执行文件可以在没有实际源代码的情况下运行。在将源代码转换为机器代码时,编译器通常会尝试优化输出,以便最终需要执行较少的机器代码。其中一种优化方式就是删除多余的机器代码来访问没有改变的变量(编译器的角度)。
对于下面这段代码:
uint32 status = 0;
while(status == 0)
{
//假设变量status在while循环中没有改变或者在整个程序中没有改变
//只要status == 0就执行这段程序
}
编译器会注意到在while
循环中,status
状态变量没有改变。所以没有必要在每次循环迭代后一次又一次地访问变量status
。所以编译器会将此循环转换为while(1)
,从而不需要机器代码来读取状态。请注意,编译器不知道该status
变量是一个特殊变量,可以在任何时间点从当前程序外部进行改变。例如某些I/O操作发生在外部设备上,设备I/O的内存映射到此变量。所以实际上,我们希望编译器在每次循环迭代后访问状态变量,即使它并没有被编译的程序修改。
有些人认为,我们可以关闭程序的编译器优化,这样就不会遇到上面的问题。但是:
- 每个编译器的实现都是不同的,不满足移植性。
- 仅仅因为一个变量就放弃整个程序的优化,得不偿失。
- 关闭掉所有优化,低级程序由于其他原因而无法执行。
所以,我们需要通过以下方式告诉编译器不要对该变量进行优化。
volatile uint32 status = 0;
一般来说,volatile
与指针一起使用。
volatile uint32* statusPtr = 0xF123000;
statusPtr
指向一个内存位置(例如对于某些I/O端口),在该位置上的内容可以随时被某个外围设备更改。我们的程序可能无法控制或了解该内存何时会发生变化。因此,我们将其设置为volatile
,防止编译器对statusPtr
指向的变量进行优化。
在C语言标准中:
具有volatile
类型的对象可能会以未知的方式修改。volatile
声明可用于描述内存映射输入/输出端口的对象或由异步中断函数访问的对象。对该对象的操作不应被实现优化掉或重新排序,除非程序允许。
基本上,C标准说volatile
变量可以从程序外部改变,这就是为什么编译器不应该优化它们的访问。
例子
volatile
关键字旨在防止编译器对可能以编译器无法确定的方式更改的对象应用任何优化。
声明为volatile
的对象从优化中省略,因为它们的值可以随时被当前代码范围之外的代码更改。即使先前的指令要求来自同一对象的值,系统也总是从内存位置读取volatile
的当前值,而不是在请求时将其值保存在临时寄存器中。
变量的值如何以编译器无法预测的方式变化。
- 被外部的中断服务程序修改的全局变量:例如,一个全局变量可以代表一个数据端口(通常称为内存映射I/O的全局指针),它将被动态更新。代码读取数据端口必须声明为
volatile
,以便获取端口上的可用的最新数据。如果未将变量声明为volatile
,编译器将优化代码,使其仅读读取端口一次,并在临时寄存器中继续使用相同的值来加速程序(速度优化)。通常,当由于新数据可用而出现中断时,中断服务程序用于更新这些数据端口。 - 多线程应用程序中的全局变量:线程通信有多种方式,例如消息传递,共享内存等。全局变量是共享内存的一种形式。当两个线程通过全局变量共享信息时,需要使用
volatile
进行限定。由于线程是异步运行的,因此一个线程对全局变量的任何更新都应该由另一个线程重新获取。编译器可以读取全局变量并将它们放置在当前线程上下文的临时变量中。为了消除编译器优化的影响,需要将此类全局变量限定为volatile
。
如果我们不使用volatile
限定符,可能会出现以下问题:
- 打开优化后,代码可能无法按预期工作。
- 启用和使用中断时,代码可能无法按预期工作。
#include <stdio.h>
int main()
{
const int local = 10;
int* ptr = (int*)&local;
printf("Initial value of local: %d \n", local);
*ptr = 100;
printf("Modified value of local: %d \n", local);
return 0;
}
未进行任何优化。
kkk@kkk-VirtualBox:~/code$ gcc volatile.c -o volatile
kkk@kkk-VirtualBox:~/code$ ./volatile
Initial value of local: 10
Modified value of local: 100
#include <stdio.h>
int main()
{
const int local = 10;
int* ptr = (int*)&local;
printf("Initial value of local: %d \n", local);
*ptr = 100;
printf("Modified value of local: %d \n", local);
return 0;
}
进行优化
kkk@kkk-VirtualBox:~/code$ gcc -O3 volatile.c -o volatile
kkk@kkk-VirtualBox:~/code$ ./volatile
Initial value of local: 10
Modified value of local: 10
kkk@kkk-VirtualBox:~/code$
#include <stdio.h>
int main()
{
const volatile int local = 10;
int* ptr = (int*)&local;
printf("Initial value of local: %d \n", local);
*ptr = 100;
printf("Modified value of local: %d \n", local);
return 0;
}
进行优化
kkk@kkk-VirtualBox:~/code$ gcc -O3 volatile.c -o volatile
kkk@kkk-VirtualBox:~/code$ ./volatile
Initial value of local: 10
Modified value of local: 100
kkk@kkk-VirtualBox:~/code$
volatile的作用?是否具有原子性,对编译器有什么影响?
volatile
的作用:当对象的值可能在程序的控制或检测之外被改变时,应该将该对象声明为volatile
,告知编译器不应该对这样的对象进行优化。
volatile
不具有原子性。
volatile
对编译器的影响:使用该关键字后,编译器不会对相应的对象进行优化,即不会将变量从内存缓存到寄存器中,防止多个线程有可能使用内存中的变量,有可能使用寄存器中的变量,从而导致程序错误。
什么情况下一定要用volatile,能否和const一起使用?
使用volatile
关键字的场景:
- 当多个线程都会用到某一个变量时,并且该变量的值有可能发生改变时,需要用
volatile
关键字对该变量进行修饰。 - 中断服务程序中访问的变量或并行设备的硬件寄存器的变量,最好用
volatile
关键字修饰。
volatile
关键字和const
关键字可以同时使用,某种类型可以既是volatile
又是const
,同时具有二者的属性。
extern C的作用
extern
是C/C++语言中表明函数和全局变量作用范围(可见性)的关键字,该关键字告诉编译器其声明的函数和变量可以在本模块或其他模块中使用。
extern "C"
的作用是实现C++语言与C语言及其他语言的混合编程。被extern "C"
修饰的变量和函数是按照C语言方式编译和链接的,而不是C++语言的方式。由于C++语言支持函数重载,因此,编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此,在编译C语言代码的函数时不会带上函数的参数类型,一般只包括函数名。
//可能出现在C++头文件<cstring>中的链接指示
extern "C"{
int strcmp(const char*, const char*);
}
memmove和memcpy的区别以及处理内存重叠问题
memcpy()
和memmove()
都是C语言中的库函数,在头文件string.h
中,作用是拷贝一定长度的内存内容,原型如下:
void* memcpy(void* dst, const void* src, size_t count);
void* memmove(void* dst, const void* src, size_t count);
它们的作用都是一样的,唯一的区别是,当内存发生局部重叠的时候,memmove
保证拷贝的结果是正确的,memcpy
不保证拷贝的结果是正确的。
有两种内存覆盖的情况:
memcpy函数
void* memcpy(void* dst, const void* src, size_t n){
char* d_dst = (char*)dst;
char* s_src = (char*)src;
while(n--)
{
*d_dst++ = *s_src++;
}
return dst;
}
对于覆盖情况1,这种拷贝方式是可以的。
memmove
针对覆盖情况2。
memmove函数
void* memmove(void* dst, const void* src, size_t size)
{
char* psrc;
char* pdst;
if(dst == nullptr || src == nullptr)
return nullptr;
if((src < dst) && (char*)src + size > (char*)dst){ //出现地址重叠的情况,从后向前复制
psrc = (char*)src + size - 1;
pdst = (char*)dst + size - 1;
while(size--)
{
*pdst-- = *psrc--;
}
}
else{
psrc = (char*)src;
pdst = (char*)dst;
while(size--)
{
*pdst++ = *psrc++;
}
}
return dst;
}
strcpy函数
char* m_strcpy(char* dst, const char* src){
assert(dst != NULL && src != NULL);
char* ret = dst;
while((*dst++=*src++) != '\0');
return ret;
}
strcpy和memcpy的区别
- 复制的内容不同。
strcpy()
只能复制字符串,而memcpy()
可以复制任何内容,例如字符数字,整型,结构体,类等。 - 复制的方法不同。
strcpy()
不需要指定长度,它遇到被复制字符串的串结束符“\0”
才结束(strcpy
会把'\0'
也复制了),所以容易溢出。memcpy()
则是根据其第3个参数决定复制的长度。 - 用途不同。通常在复制字符串时用
strcpy()
,而需要复制其他类型数据时一般用memcpy()
。
实现atoi()函数
int myAtoi(const char* str)
{
int sign = 1, base = 0, i = 0;
while(str[i] == ' ')
i++;
if(str[i] == '-' || str[i] == '+')
sign = 1 - 2 * (str[i++] == '-');
while(str[i] >= '0' && str[i] <= '9')
{
if(base > INT_MAX / 10 || (base == INT_MAX / 10 && str[i] - '0' > 7))
{
if(sign == 1)
return INT_MAX;
else
return INT_MIN;
}
base = 10 * base + (str[i++] - '0');
}
return base * sign;
}
extern关键字
extern
关键字一般用在变量名和函数名之前,作用是说明“此变量/函数”是在别处定义的,要在此处引用。
定义:标识创建变量或分配存储单元;
声明:说明变量的性质,但并不分配存储单元;
extern int i; //是声明,不是定义,没有分配内存
int i; //是定义
如果在声明的时候给变量赋值,那么就和去掉extern
直接定义变量赋值是等价的;
extern int a = 10;
int a = 10;
extern作用于函数名和变量名的区别:
- 对于函数来说,函数的定义有函数体,函数的声明没有函数体,编译器很容易区分定义和声明,对于函数声明来说,有没有
extern
都是一样; - 对于变量来说,全局变量在外部使用声明时,
extern
关键字是必须的,如果变量无extern
修饰且没有显式的初始化,就成为了变量的定义,因此此时必须加extern
。
多文件编程
头文件中包含的都是函数声明,而不是函数定义;
最好不要在头文件中定义变量,例如全局变量;
对于下面三个文件
//my.h
extern int Max(int num1, int nums2);
//my.c
#include <stdio.h>
int Max(int num1, int num2)
{
int max = num1;
if(num1 < num2)
max = num2;
return max;
}
//test1.c
#include <stdio.h>
#include "my.h"
int main()
{
int a;
a = Max(15, 20);
printf("%d\n", a);
return 0;
}
在test1.c
中,使用#include "my.h"
包含了这个头文件,然后在函数中就可以直接使用Max
函数了。
对于上面三个文件中,在my.c
文件中定义了一个全局变量,想要在main
函数中使用,该怎么做?
方法1:直接在含有main
函数的源文件test1.c
中加extern
声明
//my.h
extern int Max(int num1, int num2);
//my.c
#include <stdio.h>
int sum = 100;
int Max(int num1, int nums2)
{
int max = num1;
if(num1 < num2)
max = num2;
return max;
}
//test1.c
#include <stdio.h>
#include "my.h"
extern int sum;
int main()
{
int a;
a = Max(15, 20);
printf("%d, %d\n", a, sum);
return 0;
}
方法2:在头文件my.h
中对my.c
中的全局变量进行声明,再在test1.c
中include
头文件my.h
;
//my.h
extern int Max(int num1, int num2);
extern int sum;
//my.c
#include <stdio.h>
int sum = 100;
int Max(int num1, int nums2)
{
int max = num1;
if(num1 < num2)
max = num2;
return max;
}
//test1.c
#include <stdio.h>
#include "my.h"
extern int sum;
int main()
{
int a;
a = Max(15, 20);
printf("%d, %d\n", a, sum);
return 0;
}
防止C语言头文件被重复包含
头文件包含命令#include
的效果与直接复制粘贴头文件内容的效果是一样的,预处理器会读取头文件的内容,然后输出到#include
命令所在的位置,头文件包含是一个递归(循环)的过程,如果被包含的头文件中还包含了其他的头文件,预处理器会继续将它们也包含进来,这个过程会一直持续下去,直到不再包含任何头文件。
递归包含会导致一个问题,就是重复引入用一个头文件,重复引入同一个头文件有什么问题呢?当在头文件中定义变量或者函数时,(只能定义一次,可以声明多次),多次引入头文件就会报错——”变量被多次定义“。
解决方法
使用#if
、#endif
、#ifndef
等预处理指令;
格式如下所示
#ifndef _INC_STDIO
#define _INC_STDIO
/* 头文件内容 */
#endif
或者使用下面的方法
#pragma once
extern int Max(int num1, int num2);
int k = 10;