刷题
文章目录
volatile关键字
volatile关键字告诉编译器,该变量除了可以被长须改变以外,还可以被其他代理改变。被用于硬件地址和其他并行运行的程序共享的数据。
- 如一个地址保存着当前系统的时间,不管程序做什么,该地址的值都会随着时间而改变。
- 还可以是:一个地址被用来接收来自其他计算机的信息
- volatile可以方便编译器优化。
int val1 = x;
int val2 = x;
编译器注意到使用了两次x,而没有改变值,他将x存在一个寄存器中,当val2需要x时,可以从寄存器而非初始的内存位置中读取该值以节省时间,这个过程被称为缓存(caching),。如果没有volatile关键字,编译器可能无法得知这种改变是否发生,因此,使用关键字volatile可以令编译器每次从初始的内存位置去读取该值。
- 一个值可以是const和volatile。
- 如:硬件时钟一般设定为不能被程序改变,可以成为const。但它可以被程序以外的代理改变,因此可以成为volatile。声明中加两个限定词的顺序无关。
- volatile const int loc;
- const volatile int * loc;
restrict关键字
- 只可用于指针,表明指针是访问一个数据对象的唯一且初始的方式。一般在使用malloc时使用。
- int * restrict restar = (int * )malloc(10 * sizeof(int));
- 指针restar是访问由malloc分配的内存的唯一且初始的方式。
- 应用场景:
- restart是访问它所指向数据块的唯一初始化方式,编译器就可以用具有同样效果的一条语句来代替包含restar语句。(编译器优化)
- void * memcpy(void* restrict dest, const void * restrict src, size_t n); // #include<string.h>
- memcpy函数要求两个位置不重叠,把dest 和 src 声明为restrict意味着每个指针都是相应数据的唯一访问方式,因此他们不能访问同一个数据块,这就满足了不能有重叠的要求。
字符串函数strlen、cat等00
- strlen(name)获取的是字符串的长度(带‘\0’的长度,如"hello"的长度是6)
- 字符数组(元素为为单个字符如’A’… 没有’\0’结束符),字符串数组(如"AFB…",自带’\0’结束符)
- strcat() :代表string concatenation,将第二个字符串的一份拷贝添加到第一个字符串的结尾,从而使第一个字符串成为一个新的组合字符串。
- strcmp():两者相同返回0,第一个字符串在字母表(ASCII)的顺序先于第二个字符串,则返回负数,负责返回正数,以第一个字符串的’\0’作为比较结束标志,如果第一个字符串长,则返回的是正数(第一个字符串的数据和第二个字符串的’\0’相比较的结果,‘\0’在ASCII中最小,任意字符一定在其后面,如strcmp(“hs”,“h”);此时’s’和’\0’比较后退出返回正数)。如strcmp(“A”,“B”),返回的为-1;
字符串数组初始化的差别:
- char m3[] = “ADFG”; //程序被加载到内存时,将"ADFG"从静态存储区(可执行文件的数据段)拷贝到数组m3中。注意:是拷贝完整数据到数组中!
- char * m3 = “ADFG”; //程序开始执行时,预留一个存储位置,在该指针遍历中存储字符串的地址,指针初始化时指向字符串的第一个字符。注意:只赋值字符串的地址。
- 区别在于指针型m3可以使用自增运算符进行数据的遍历!
- 如果程序多个 地方使用到了“ADFG”,那么通过指针m3修改内容时会影响所有!,故指针型初始化,如果后面不需要修改对象,最好采用const char * m3 = "ADFG"这种类型。
字符串数组:
const char * mytal[3] = {“hello”,“ca”,“wunai”};const char mytal2[3][10] = {“hello”,“ca”,“wunai”};
- mytal是一个由3个指向char型的指针组成的数组
- mytal是一个一维数组,数组里每一个元素都是char类型的地址
- mytal[0],指向第一个字符串的第一个字符’h’对应的地址,即 * mytal[0] == ‘h’
- mytal并不存放字符串,只是存放字符串的地址(字符串存在程序用来存放常量的那部分内存中),可以把mytal[0]看作表示第一个字符串,* mytal[0]表示第一个字符串的第一个字符,由于数组符合和指针的关系,也可以用 mytal[0][0]表示第一个字符串的第一个字符,尽管没有被定义为二维数组。
- mytal是存放3个地址,mytal2是存放3个完整的字符数组。
递归、斐波那契数列
递归优点:为某些编程问题提供了最简单的解决方法
递归缺点:递归算法会很快耗尽计算机的内存资源,同时递归程序难于阅读和维护
- 斐波那契数列
- 定义:数列内第一个和第二个数字都是1,后续的每个数字是其前两个数字之和(数列内前几个数字为: 1、1、2、3、5、8…)。
进程间通信
Linux使用较多的进程间通信方式:
- 管道(Pipe)及有名管道(named pipe)
- 信号(signal),在软件层次上对中断机制的一种模拟,是一种异步通信方式。
- 消息队列(message queue)
- 共享内存(shared memory),多个进程访问同一块内存空间,依靠同步机制:如互斥锁/信号量等
- 信号量(semaphore)
- 套接字(socket)
进程和线程
进程是一个独立的可调度的活动
进程是一个抽象实体,当它执行某个任务时,要分配和释放各种资源
进程是可以并行执行的计算单位
进程是资源分配的最小单元。它和程序的本质区别是:
- 程序是静态的,它是保存在磁盘上的指令的有序集合,没有任何执行的概念
- 进程是动态的,它是程序执行的过程,包括了动态创建、调度和消亡的整个过程
- 是程序执行和资源管理的最小单位
- 当系统激活执行一个指令的时候,它将启动一个进程
线程:是进程内独立的一条运行路线,处理器调度的最小单元。线程可以对进程的内存空间和资源进行访问,并与同一进程中的其他线程共享。因此线程的上下文切换的开销比创建进程小很多。
线程间的同步与互斥:
- 互斥锁,适合用于同时可用的资源是唯一
- 信号量。适合用于同时可用的资源为多个
大小端对union类型数据的影响
union型数据所占的空间等于其最大的成员所占的空间,对union型成员的存取都从相对于该联合体基地址的偏移量为0开始,也就是说对于联合体的访问,不论是哪个变量都是从union的首地址开始,所以机器大小端的不同会对union型的数据产生影响。
大端(Big_endian)字数据的高字节存储在低地址中,字数据的低字节存储在高地址中。
小端(Little_endian)字数据的高字节存储在高地址中,字数据的低字节存储在低地址中。
使用联合体检测是大端还是小端:
#include <iostream>
using namespace std;
union MyUnion
{
char a;
int b;
};
int main()
{
union MyUnion stu;
stu.b = 0; //先把联合体数据清0
stu.a = 1; // 再对小长度字节(低地址) 赋值, 判断b是否等于1,;反过来赋值b,判断小字节的值也可以(是否赋值在了低字节)。
cout << "stu: " << sizeof(stu) << "\t Addr stu: " << &stu << endl;
cout << "Num stu.a: " << (void *)stu.a << "\t Size stu.a: " << sizeof(stu.a) << "\t Addr stu.a: " << (void*)&stu.a << endl;
cout << "Num stu.b: " << (void *)stu.b << "\t Size stu.b: " << sizeof(stu.b) << "\t Addr stu.b: " << &stu.b << endl;
while (true)
{
}
return 0;
}
void* 指针
在函数的返回值中, void 是没有任何返回值, 而 void * 是返回任意类型的值的指针
- 1、void的作用
c语言中,void为“不确定类型”,不可以用void来声明变量。如:void a = 10;如果出现这样语句编译器会报错:variable or field ‘a’ declared void。
在C语言中void 常常用于:对函数返回类型的限定和对函数参数限定
(1)对函数返回类型的限定:当函数不需要返回类型是必须用void 来限定返回类型,限定了函数的返回类型为void后函数不能有返回值;如:void fun(int a);
(2)对函数参数类型的限定:当函数不允许接受参数时必须用void 来限定函数参数,限定了函数的参数类型为void后函数不能有参数;如:int fun(void); - 2、在C语言中void *是一个很有意思的玩意。
C语言中void * 为 “不确定类型指针”,void *可以用来声明指针。如:void * a;
(1)void * 可以接受任何类型的赋值:
void * a = NULL;
int * b = NULL;
a = b;// a是void * 型指针,任何类型的指针都可以直接赋值给它,无需进行强制类型转换,我们可以认为void就是一张白纸,可以在上面写任何类型的数值。
(2)void * 可以赋值给任何类型的变量 但是需要进行强制转换:
例:
int * a = NULL ;
void * b ;
a = (int * )b;
但是有意思的是:void * 在转换为其他数据类型时,赋值给void * 的类型 和目标类型必须保持一致。简单点来说:void * 类型接受了int * 的赋值后 这个void * 不能转化为其他类型,必须转换为int * 类型;
如:
void * memcpy(void *dest, const void *src, size_t len);
void * memset ( void * buffer, int c, size_t num );
这样,任何类型的指针都可以传入memcpy和memset中,这也真实地体现了内存操作函数的意义,因为它操作的对象仅仅是一片内存,而不论这片内存是什么类型(参见C语言实现泛型编程)。
ostream为const signed char *、const unsigned char *、const char *、void *重载了<<操作符,因此,可以使用cout<<输出显示字符串;这个方法使用\0来判断是否停止显示字符。
如果要显示字符串的地址,由于传递指针输出了整个字符串,因此将其强制转换为void *类型可以显示字符串的地址。
cout << "输出与指针*********************************************************************" << endl;
int eggs = 12;
const char* amount = "dozen";
cout << &eggs; // prints address of eggs variable
cout << amount; // prints the string "dozen"
cout << (void*)amount<<endl; // prints the address of the "dozen" string
结构体对齐方式
- 对齐原则:
- 单字节(char)变量无需对齐,可放在任何位置
- 双字节(short)变量起始地址为2的倍数
- 4字节(int)变量首地址为4的倍数
- 8字节(double)变量首地址为8的倍数
- 检查计算出的存储单元是否为所有元素中最宽的元素的长度的整数倍,是,则结束;不是,则补齐为它的整数倍。
一个结构体变量定义完之后,其在内存中的存储并不等于其包含元素的宽度之和。
#include <iostream>
using namespace std;
struct MyStruct
{
char a;
int b;
double c;
};
int main()
{
struct MyStruct stu;
stu.a = 1;
stu.b = 2;
stu.c = 3;
cout << "stu: " << sizeof(stu) << "\t Addr stu: " << &stu << endl;
cout << "stu.a: " << sizeof(stu.a) << "\t Addr stu.a: " << &stu.a << endl;
cout << "stu.b: " << sizeof(stu.b) << "\t Addr stu.b: " << &stu.b << endl;
cout << "stu.c: " << sizeof(stu.c) << "\t Addr stu.c: " << &stu.c << endl; // 未知原因,发现此时c地址乱码(在结构体变为c是char型时,cout认为是字符型,所以输出的是字符串,但是没有\0结尾符导致输出乱码)(解决方案:cout重载了很多类型,对于char *,它会当作以’\0’结尾的字符串来处理以便于cout输出。char a = 'c'类型取地址修改为:cout << (void *)&a << endl;)void指针可以指向任意类型的数据,就是说可以用任意类型的指针对void指针对void指针赋值。。
printf("Addr stu.c: %0X\n",&stu.c); //用此方法可以查看到c地址。
while (true)
{
}
return 0;
}
经测试,会发现sizeof(S1)= 16,其值不等于sizeof(S1.a) = 1、sizeof(S1.b) = 4和 sizeof(S1.c) = 8三者之和,这里面就存在存储对齐问题。
原则一:结构体中元素是按照定义顺序一个一个放到内存中去的,但并不是紧密排列的。从结构体存储的首地址开始,每一个元素放置到内存中时,它都会认为内存是以它自己的大小来划分的,因此元素放置的位置一定会在自己宽度的整数倍上开始(以结构体变量首地址为0计算)。如图1所示,b(int)存储时,认为内存是以int大小划分,则地址从0以4字节的步长来计算,获得此时b的地址为偏移4字节的位置。
原则二:在经过第一原则分析后,检查计算出的存储单元是否为所有元素中最宽的元素的长度的整数倍,是,则结束;若不是,则补齐为它的整数倍。
对C语言内内存分配与使用的理解及注意事项
程序的栈区域是有限的,声明局部变量有时并不能满足所需的内存大小,这时就需要用户在堆区自己分配。
注意事项:内存分配完后,结束使用时一定记得释放,避免内存泄漏;如果有指针指向分配的内存,内存释放后,也需将指针释放,避免出现野指针。
进程与线程的区别
- 进程和线程都是一个时间段的描述,是cpu工作时间段的描述。
- 线程在进程下进行(线程:车厢,进程:车头)
- 一个进程可以包含多个线程
- 不同进程间数据很难共享,进程间不会相互影响。但一个线程挂掉将导致整个进程挂掉
- 进程使用的内存地址可以上锁,即一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存(火车上的洗手间)—“互斥锁”
- 进程使用的内存地址可以限定使用量(如火车上的餐厅最多允许几人同时就餐)-- “信号量”
关键字static的作用
- 修饰函数的局部变量,特点:有默认值0,只执行一次,运行一开始就开辟了内存,内存放在全局
- 修饰全局函数和变量,特点:只能在本源文件使用
- 修饰类里面的成员变量,特点:不进入类的大小计算,不依赖于类对象的存在而存在
关键字const
const :程序会把数据当常量来处理,在声明const量的适合必须进行初始化,声明之后,不能再对它赋值。
- 函数形参声明中尽量使用const数组,const并不要求原始数组是固定不变的,只是说明函数在处理数组时,应把数组当作是固定不变的。告知编译器,应把该形参作为包含常量的数组对待。
- 如果函数不需要修改数组,那么在声明数组形参时最好使用const
- 指向常量的指针不能用于修改数值:const double * pd = rates;无法使用pd来修改所指向数据的值,但是可以让指针指向别的地址(常量或者非常量数据的地址都可以赋给指向常量的指针!但是只有非常量的地址才可以赋给普通指针!)。
- 通常把指向常量的指针用作函数形参,以表面不会用这个指针修改数据:void test(const double * p,int n);
- 使用const来声明并初始化指针,以保证指针不会指向别处:double * const pc = rates;pc这样的指针可以修改rates数据,但只能指向rates这个最初赋给它的地址。
- 使用两个const创建指针,则指针不可以更改指向的地址,也不可以修改所指向的数据:const double * const pc = tates;
typedef和#define
- 与#define不同不同,typedef给出的符号名称仅限于对类型,而不是对值。
- typedef的解释由编译器,而不是预处理器执行
- typedef与define的区别:
- typedef char * STRING
- STRING name,sign;
- 将被翻译为:char * name , * sign;
- 如果没有关键字typedef,该例将STRING识别为一个char 指针。有了这个关键字,使STRING成为一个char指针类型的标识符。
- #define STRING char *
- STRING name,sign;
- 将被翻译为:char * name,sign;在这种情况下,只有name是一个指针。
- 使用结构体时:
typedef struct com{
int a;
double b;
}COM;
这样就可以使用COM来代替struct com来表示了。
使用typedef的原因之一是为经常出现的类型创建一个方便的、可识别的名称。
使用typedef来命名一个结构体类型时,可以省去结构的标记。
typedef struct{
int a;
double b;
}COM;
COM c1(3,6.1); //该语句被翻译为:struct{int a;double b;} r1 = {3,6.1};
什么是预编译,何时需要预编译
- 预编译又称预处理,是做代码文本的替换工作,处理#开头的指令。如拷贝#include头文件代码,#define宏定义的替换,条件编译等,在程序开始编译前进行。
- ##运算符可以用于类函数宏的替换部分,把两个语言符号组合成单个语言符号
- #define XNAME(n) (x ## n)
自旋锁和互斥锁的区别
- 自旋锁比较适用于锁使用者保持锁时间比较短的情况。由于锁时间短,因此选择自旋而不是睡眠是非常有必要的,效率远高于互斥锁。
- 自旋锁在保持期间是抢占失效的,而信号量和读写信号量在保持期间是可以被抢占的。
- 在单cpu且不可抢占的内核下,自旋锁的所有操作都是空操作,故而自旋锁只有内核可抢占和SMP(多处理器)的情况下才有效。
指针数组
数组初始化:
- 如果定义时不初始化数组,则存储的是无用的随机数值。
- 如果初始化了部分元素,则未初始化的元素被设置为0,
还可以int arr[6] = {[5]=211};这样去初始化特定元素的值,此为初始化arr数组内第5个地址对应元素的值为211。
优先级问题:
其 * p++ , 其中* 和++具有相等的优先级,但结合顺序是自右向左进行的,也就是++应用于指针,而不是指针指向的元素数据。后缀形式是p++,而不是++p,即先把指针指向的元素数据提出来使用后,在指针自增1。如果是*++p的话则是先指针自增1,再取指针指向的元素数据。
(* p)++:使用p指向的数据,再使数据自增1
对指针加一,等价于对指针的值加上它所指向的对象的字节大小,如int * p ; p++; 则是p的地址移动4个字节。指针加n的话就是4乘以n个字节。指向同一数组不同位置的指针相减:差值的单位是响应类型的大小,如int数组,差值为2代表的是两个指针所指对象的距离为2个int类型的大小,不是2个字节!!!
在此看出:程序中*p++是先提取p指向的元素数据如M,然后再指针加1,此时取p的地址则是0073F961,而不是0073F960;(打印地址可以使用%p(C99标准打印地址方法,默认16进制) ,或者使用 %u , %lu甚至%d,%x都是可以的)。
int * p[5]与 int (* p)[5]的区别
-
C语言中,[]和()的优先级比 * 的优先级高,且他们是从左到右结合的,故 int (* p)[5]是先将 * 和p结合起来成为一个指针,该指针指向一个具有5个int值的对象。
-
int * p[5]定义一个指针数组p,数组内的元素是5个指针,而数组内的每一个指针指向一个int型的变量,每一个数组指针变量都和普通变量没有区别。
-
int (* p)[5]定义了一个指针p,p所指向的对象使包含5个int型元素的一维数组。下图可看出,p就是一个指针,指向的是二维数组中num[1]这个对象的地址,这个对象是包含5个int型元素的。即p是一个二维指针,指向二维数组,一维指针就可以指向一维数组。
同理:函数指针与指针函数:int * p()函数和int(* p)()有什么区别 -
int * fun(int a,int b)是一个函数,只不过返回值为指针类型(加 * 的fun函数为指针函数,其返回的指针指向一个整型变量)
#include<stdio.h>
int main()
{
int a = 4, b = 5;
int *pluse; //指针Pluse用来接收指针函数返回的值
int* sun(int* p, int* q); //声明一个指针函数
pluse = sun(&a, &b);
printf("%d\n", *pluse);
}
int* sun(int* p, int* q)
{
int s;
s = *p + *q;
return &s; //返回s的地址
}
- 每个函数都要占用一段连续的存储空间,而函数名就是这段内存区域的首地址,如果把这个首地址赋给一个指针变量,那就可以调用指针来实现函数的功能了。而这样的函数叫做函数指针(把函数当成一个指针来看),其形式就是int(* p)()。
- int (* p)(int, int); //定义一个函数指针
- 函数调用时“(* 指针变量名)”两边的括号一定一定不能少,其中的“ * ”不要理解为指针运算,此时的它只是一种指示符号。
#include<stdio.h>
int max(int a, int b) //取a,b的最大值
{
if (a > b) return a;
else return b;
}
int main()
{
int a = 1, b = 2;
int z;
int (*p)(int, int); //定义一个函数指针
p = max; //这个函数指针指向定义的函数
z = (*p)(a, b); // *不能少
printf("%d\n", z);
return 0;
}
由于数组名就是数组首元素的地址,所以如果实际参数是一个数组名,那么形式参数是与之相匹配的指针,在此清空下,int ar[]和int * ar 做出同样的解释,ar是指向int的指针。
声明函数时,名称可以省略,但是定义函数时,名称是不可以省略的。
以下原型等价:
- int sum(int* ar, int n);
- int sum(int*, int);
- int sum(int ar[], int n);
- int sum(int [], int);
指针容易搞错的点:
指针数组:主语是数组,数组里面每个元素都是指针。
- int* a[10]; //指针数组,数组内每个元素都是指针!
数组指针:主语是指针,是一个指针,数组指针指向的是数组中的一个具体元素,它其实还是一个指针,只不过是指向数组而已;只要是一个指针指向了数组,我们就称为数组指针,常用于遍历数组。
- int arr[5];
- int * p = arr; //p就是一个数组指针,当前指向数组首地址元素
char* ptr; // ptr是指针变量,是一个int值。 ptr指向char型变量。
在windows中,一个int占4个字节,char占1个字节,short占2个字节;
#include<stdio.h>
void main()
{
//a是一个包含3个元素的数组,每个元素的类型是int[4]。故a[0]代表是包含4个int数值,第一个数值索引为a[0][0]。
//数组名同时也是该数组首元素的地址。
int a[3][4] = { 1,2,3,4,5,6,7,8,9,10,11,12 }; //数组下标数字也称为索引,从0开始计数。
int* ptr = a[0]; //指向a[0][0]的地址. 等同于:int* ptr = &a[0][0]; --- a[0][0]是取对应地址的值,a[0]是一个元素,是一个地址,这个元素是数组,a[0]就是元素名。
// 定义一个p指针,指向具有4个元素的一维数组
int(*p)[4] = (a + 1); //分别指向a[1][0] a[1][1] a[1][2] a[1][3]
printf("%d\n", p); //指针占用一个int型字节大小来存储指向的地址。 对指针加1,等价于对指针的值加上它指向的对象的字节大小。
printf("%d\n", *p); //*(p+n):表示寻找内存中的p指向的地址,移动n个单位(根据指向对象的字节),再取出数值。 等价于 p[n]
// *p+2 由于间接运算符*的优先级高于+,因此等价于(*p)+2 ;
//谨记, 2维数组, 需要**才取到值, 或者[][] , 或者*p[]
printf("%d\n", *(ptr + 10)); // a[0][0] 偏移10个地址, a[2][2]=11
printf("%d\n", (*p + 1)[2]); // p存储的是a[1]的地址,*p指向a[1]地址指向的地址,即a[1][0] , [2]代表取偏移2地址的数据,即 a[1][1+2] = 8
printf("%d\n", *(*(a + 2) + 1)); // a[2][1] = 10
}
深、浅复制的问题
浅复制:不同的对象指向同一块内存地址;故会产生相互影响。
深复制:系统开辟新的内容空间,来存储和目标对象相同的内容。
class A
{
int i;
};
class B
{
A *p;
public:
B(){p=new A;}
~B(){delete p;}
};
void sayHello(B b)
{
}
int main()
{
B b;
sayHello(b); // 执行sayHello(b);该函数接收参数b,函数中this->b= b;属于浅复制,它们指向同一块内存区域。当sayHello(b);函数运行结束时,系统删除this->b指向的内存区域,而后回到main,类b的析构函数删除b指向的内存区域,但是这块区域在sayHello(b);运行结束时,就已经被删除了,所以程序报错。
}
// 添加一个复制构造函数,实现深复制。
class B
{
A *p;
public:
B(){p=new A;}
B(const B &v){p=new A;} // 复制一个B类对象
~B(){delete p;}
};
传址调用/引用调用/值传递
- 传引用 和 传地址
- 相同点:原理上都是将成参数变量的地址传递给被调函数,所以在函数内部修改参数的值时,均可返回修改之后的结果给调用者。
- 不同点:
- 引用必须初始化,指针不用(可能是NULL)
- 引用初始化后不能修改,指针可以改变所指对象
- 引用本身就是目标变量的别名,对引用高度操作就是对目标变量的操作
- 传引用时,函数参数需要写作T & a,调用函数时直接传递对象本身,在函数内赋值的时候,直接对a进行赋值即可,系统对传过来的参数不会有任何额外的开销,直接使用原变量的内存空间;传地址时,函数参数需要写作T * p,调用函数时需要传入对象地址,赋值时需要对* p赋值。
void func(int *p)
{
static int num = 4;
p = # //地址被更改
(*p)--;
}
int main()
{
int i = 5;
int *p = &i;
func(p);//p指向的是一个地址,传地址调用
printf("%d", *p); //输出: 5 ,更改数据的不是p指向的这个地址
return 0;
}