在学习的时候找到几个十分好的工程和个人博客,先码一下,内容都摘自其中,有些重难点做了补充!
才鲸 / 嵌入式软件笔试题汇总
嵌入式与Linux那些事
阿秀的学习笔记
小林coding
百问网linux
嵌入式软件面试合集
2022年春招实习十四面(嵌入式面经)
说明:C++更多的内容后面再补充,平时还是主攻C比较多,等系统学完再补充。 还有很多面试具体真题,待把其它基础点更完再一一补充。力争做到一步到位!
作用:将宏定义中的传入参数名转换成用一对双引号括起来参数名字符串。其只能用于有传入参数的宏定义中,且必须置于宏定义体中的参数名前。
举例:
#define example( instr ) printf( "the input string is:\t%s\n", #instr )
#define example1( instr ) #instr当使用该宏定义时:
example( abc ); // 在编译时将会展开成:printf("the input string is:\t%s\n","abc")
string str = example1( abc ); // 将会展成:string str="abc"
作用:将宏定义的多个形参转换成一个实际参数名。
举例:
#define exampleNum( n ) num##n
int num9 = 9;
int num = exampleNum( 9 ); // 将会扩展成 int num = num9
#define exampleNum( n ) num ## n
// 相当于 #define exampleNum( n ) num##n
#include
#include
#define STRCPY(a, b) strcpy(a ## _p, #b)
int main()
{
char var1_p[20];
char var2_p[30];
strcpy(var1_p, "aaaa");
strcpy(var2_p, "bbbb");
STRCPY(var1, var2);
STRCPY(var2, var1);
printf("var1 = %s\n", var1_p);
printf("var2 = %s\n", var2_p);
//STRCPY(STRCPY(var1,var2),var2);
//这里是否会展开为: strcpy(strcpy(var1_p,"var2")_p,"var2“)?答案是否定的:
//展开结果将是: strcpy(STRCPY(var1,var2)_p,"var2")
//## 阻止了参数的宏展开!如果宏定义里没有用到 # 和 ##, 宏将会完全展开
// 把注释打开的话,会报错:implicit declaration of function 'STRCPY'
return 0;
}
结果:
var1 = var2
var2 = var1
(1)volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。
当要求使用 volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。
volatile定义变量的值是易变的,每次用到这个变量的值的时候都要去重新读取这个变量的值,而不是读寄存器内的备份。多线程中被几个任务共享的变量需要定义为volatile类型。
(2)volatile 指针
volatile 指针和 const 修饰词类似,const 有常量指针和指针常量的说法,volatile 也有相应的概念修饰由指针指向的对象、数据是 const 或 volatile 的:
const char* cpch;volatile char* vpch;
针自身的值——一个代表地址的整数变量,是 const 或 volatile 的:
char* const pchc;char* volatile pchv;
注意:
(3)多线程下的volatile
有些变量是用volatile关键字声明的。当两个线程都要用到某一个变量且该变量的值会被改变时,应该用volatile声明,**该关键字的作用是防止优化编译器把变量从内存装入CPU寄存器中。**如果变量被装入寄存器,那么两个线程有可能一个使用内存中的变量,一个使用寄存器中的变量,这会造成程序的错误执行。volatile的意思是让编译器每次操作该变量时一定要从内存中真正取出,而不是使用已经存在寄存器中的值
XBYTE[2]=0x55;
XBYTE[2]=0x56;
XBYTE[2]=0x57;
XBYTE[2]=0x58;
对外部硬件而言,上述四条语句分别表示不同的操作,会产生四种不同的动作,但是编译器却会对上述四条语句进行优化,认为只有XBYTE[2]=0x58(即忽略前三条语句,只产生一条机器代码)。如果键入volatile,编译器会逐一的进行编译并产生相应的机器代码(产生四条代码)。
static
const
不考虑类的情况
考虑类的情况
补充一点const相关:const修饰变量是也与static有一样的隐藏作用。只能在该文件中使用,其他文件不可以引用声明使用。 因此在头文件中声明const变量是没问题的,因为即使被多个文件包含,链接性都是内部的,不会出现符号冲突。
const int a;//a是一个常整型数
int const a;//a是一个常整型数
const int*a; //常量指针,指向常量的指针。即p指向的内存可以变,p指向的数值内容不可变
int const*a; //同上
int *const a; //a是一个指向整型数的常指针(也就是说,指针指向的整型数是可以修改的,但指针是不可修改的)。
int const *const a ; //a是一个指向常整型数的常指针(也就是说,指针指向的整型数是不可修改的,同时指针也是不可修改的)
const int *const a; //a是一个常量指针,且指向整型常量 ---> p是一个指向整型常量的常量指针(同上)
指针常量和常量指针
extern放在变量和函数声明之前,表示该变量或者函数在别的文件中已经定义,提示编译器在编译时要从别的文件中寻找。除此之外,extern还可以用来进行链接指定。
extern在链接阶段起作用(四大阶段:预处理–编译–汇编–链接)。
C语言extern关键字用法和理解
extern "C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言的进行编译,而不是C++的。
用法举例:
(1)C++调用C函数:
//xx.h
extern int add(...)
//xx.c
int add(){
}
//xx.cpp
extern "C" {
#include "xx.h"
}
(2)C调用C++函数
//xx.h
extern "C"{
int add();
}
//xx.cpp
int add(){
}
//xx.c
extern int add();
strlen与 sizeof的差别表现在以下5个方面。
struct(结构体)与 union(联合体)是C语言中两种不同的数据结构,两者都是常见的复合结构,其区别主要表现在以下两个方面。
内存对齐作用:
1.平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2.性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
1.对于结构体的各个成员,第一个成员的偏移量是0,排列在后面的成员其当前偏移量必须是当前成员类型的整数倍;
2.结构体内所有数据成员各自内存对齐后,结构体本身还要进行一次内存对齐,保证整个结构体占用内存大小是结构体内最大数据成员的最小整数倍;
3.如程序中有#pragma pack(n)预编译指令,则所有成员对齐以n字节为准(即偏移量是n的整数倍),不再考虑当前类型以及最大结构体内类型。
举例
#pragma pack(1)
struct fun{
int i;
double d;
char c;
};
sizeof(fun) = 13
struct CAT_s
{
int ld;
char Color;
unsigned short Age;
char *Name;
void(*Jump)(void);
}Garfield;
1.使用32位编译,int占4, char 占1, unsigned short 占2,char* 占4,函数指针占4个,由于是32位编译是4字节对齐,所以该结构体占16个字节。(说明:按几字节对齐,是根据结构体的最长类型决定的,这里是int是最长的字节,所以按4字节对齐);
2.使用64位编译 ,int占4, char 占1, unsigned short 占2,char* 占8,函数指针占8个,由于是64位编译是8字节对齐(说明:按几字节对齐,是根据结构体的最长类型决定的,这里是函数指针是最长的字节,所以按8字节对齐)所以该结构体占24个字节。
//64位
struct C
{
double t; //8 1111 1111
char b; //1 1
int a; //4 0001111
short c; //2 11000000
};
sizeof(C) = 24; //注意:1 4 2 不能拼在一起
char是1,然后在int之前,地址偏移量得是4的倍数,所以char后面补三个字节,也就是char占了4个字
节,然后int四个字节,最后是short,只占两个字节,但是总的偏移量得是double的倍数,也就是8的倍
数,所以short后面补六个字节
1.找到占用字节最多的成员;
2.union的字节数必须是占用字节最多的成员的字节的倍数,而且需要能够容纳其他的成员。
//x64
typedef union {
long i;
int k[5];
char c;
}D
要计算union的大小,首先要找到占用字节最多的成员,本例中是long,占用8个字节,int k[5]中都是int类型,
仍然是占用4个字节的,然后union的字节数必须是占用字节最多的成员的字节的倍数,而且需要能够容纳
其他的成员,为了要容纳k(20个字节),就必须要保证是8的倍数的同时还要大于20个字节,所以是24个字
节。
C语言允许在一个结构体中以位为单位来指定其成员所占内存长度,这种以位为单位的成员称为“位段”或称“位域”( bit field) 。利用位段能够用较少的位数存储数据。一个位段必须存储在同一存储单元中,不能跨两个单元。如果第一个单元空间不能容纳下一个位段,则该空间不用,而从下一个单元起存放该位段。
1.位段声明和结构体类似;
2.位段的成员必须是int、unsigned int、signed int;
3.位段的成员名后边有一个冒号和一个数字。
typedef struct_data{
char m:3;
char n:5;
short s;
union{
int a;
char b;
};
int h;
}_attribute_((packed)) data_t;
答案12
m和n一起,刚好占用一个字节内存,因为后面是short类型变量,所以在short s之前,应该补一个字节。所以m和n其实是占了两个字节的,然后是short两个个字节,加起来就4个字节,然后联合体占了四个字节,总共8个字节了,最后int h占了四个字节,就是12个字节了
attribute((packed)) 取消对齐
GNU C的一大特色就是attribute机制。attribute可以设置函数属性(Function Attribute)、变量属性(Variable Attribute)和类型属性(Type Attribute)。
attribute书写特征是:attribute前后都有两个下划线,并且后面会紧跟一对括弧,括弧里面是相应的attribute参数。
跨平台通信时用到。不同平台内存对齐方式不同。如果使用结构体进行平台间的通信,会有问题。例如,发送消息的平台上,结构体为24字节,接受消息的平台上,此结构体为32字节(只是随便举个例子),那么每个变量对应的值就不对了。
不同框架的处理器对齐方式会有不同,这个时候不指定对齐的话,会产生错误结果
左值是指可以出现在等号左边的变量或表达式(例如变量、数组元素、结构体成员等),它最重要的特点就是可写(可寻址)。也就是说,它的值可以被修改,如果一个变量或表达式的值不能被修改,那么它就不能作为左值。
右值是指只可以出现在等号右边的变量或表达式(例如一个常量、字面量、表达式的计算结果等)。它最重要的特点是可读。一般的使用场景都是把一个右值赋值给一个左值。
通常,左值可以作为右值,但是右值不一定是左值。
在 C 语言中,左值和右值在语法上有一些差别。例如,左值可以出现在地址运算符 & 的操作数中,而右值不能。例如:
int a = 1;
int *p = &a; // &a 是一个左值,可以取地址
int *q = &1; // 错误:&1 是一个右值,不能取地址
需要注意的是,有些表达式既可以是左值也可以是右值,这取决于上下文。例如,数组名可以被用作左值(例如 a[0] = 1;),也可以被用作右值(例如 int *p = a;)。
下面的输出结果是多少:
#include
int main()
{
int i = 6;
int j = 1;
if(i>0||(j++)>0);
printf("%D\r\n",j);
return 0;
}
参考答案:输出1
说明:
a++和++a 都是自增运算符,俩者的区别在于对变量a的值进行自增的时机不同==> a++是先取值后自增; ++a是先自增后取值。
后置自增运算符需要把原来变量的值复制到一个临时的存储空间,等运算结束后才会返回这个临时变量的值。所以前置自增运算符效率比后置自增要高
举例如下:
例1:var a=10 ; b=20; c=4; 求++b+c+a++的值
++b=21;a++=10;c=4; => ++b+c+a++=21+4+10=35;
例2:var a=10, b=20 , c=30;
++a;//①
a++;//②
e=++a+(++b)+(c++)+a++;//③
alert(e);
①=> a=11(自增后取值为11);
②=> a=11(直接取值,此时a在下次运算时自增值为12);
③=> ++a=13 ++b=21 c++ = 30 a++=13
所以e=77
代码区:也称为文本区,存储程序的可执行代码,通常是只读的。在程序运行时,代码区的内容被加载到内存中,并且只能被执行,不能被修改。
数据区:也称为静态区,存储程序中已经初始化的全局变量、静态变量、常量等。数据区在程序启动时被分配,并且在程序结束时被释放。数据区分为只读数据区和可读写数据区。只读数据区存储不能被修改的常量和字符串,而可读写数据区存储可以被修改的全局变量、静态变量等。
堆区:是由程序员自行申请和释放的内存空间。在程序运行时,堆区的内存空间是动态分配的,可以通过 malloc、calloc、realloc 等函数进行分配。堆区的内存空间需要手动释放,否则会导致内存泄漏。
栈区:存储程序的局部变量、函数参数等。在函数调用时,栈区会自动分配一定的内存空间,当函数返回时,这些内存空间会被自动释放。栈区的大小是固定的,通常比堆区小得多。
静态分配:在程序编译时就分配好内存,通常用于定义全局变量、静态变量、常量等。静态分配的内存在程序启动时就已经分配好,直到程序结束时才会释放。
栈上分配:在函数调用时分配内存,通常用于定义局部变量、函数参数等。栈上分配的内存在函数执行完毕时就会自动释放,因此不需要手动释放。
堆上分配:在程序运行时根据需要动态分配内存,通常使用 malloc、calloc、realloc 等函数进行内存分配。堆上分配的内存需要手动释放,否则会导致内存泄漏。
堆(Heap)和栈(Stack)是计算机内存中两种不同的内存管理方式,在内存分配、使用、释放等方面存在以下几点区别:
malloc
、calloc
、realloc
等)进行内存分配。在C++中,虚拟内存分为代码段、数据段、BSS段、堆区、文件映射区以及栈区
六部分。
代码段:包括只读存储区和文本区,其中只读存储区存储字符串常量,文本区存储程序的机器代码。
数据段:存储程序中已初始化的全局变量和静态变量
BSS 段:存储未初始化的全局变量和静态变量(局部+全局),以及所有被初始化为0的全局变量和静态
变量。
堆区:调用new/malloc函数时在堆区动态分配内存,同时需要调用delete/free来手动释放申请的内存。
映射区:存储动态链接库以及调用mmap函数进行的文件映射
栈:使用栈空间存储函数的返回地址、参数、局部变量、返回值
简单地说就是申请了一块内存空间,使用完毕后没有释放掉。
它的一般表现方式是程序运行时间越长,占用内存越多,最终用尽全部内存,整个系统崩溃。由程序申请的一块内存,且没有任何一个指针指向它,那么这块内存就泄露了。
避免内存泄露的几种方式:
小端:
内存地址 | 0x4000 | 0x4001 | 0x4002 | 0x4003 |
---|---|---|---|---|
存放内容 | 0x78 | 0x56 | 0x34 | 0x12 |
大端:
内存地址 | 0x4000 | 0x4001 | 0x4002 | 0x4003 |
---|---|---|---|---|
存放内容 | 0x12 | 0x34 | 0x56 | 0x78 |
方式一:使用强制类型转换
#include
using namespace std;
int main()
{
int a = 0x1234;
//由于int和char的长度不同,借助int型转换成char型,只会留下低地址的部分
char c = (char)(a);
if (c == 0x12)
cout << "big endian" << endl;
else if(c == 0x34)
cout << "little endian" << endl;
}
方式二:巧用union联合体
#include
using namespace std;
//union联合体的重叠式存储,endian联合体占用内存的空间为每个成员字节长度的最大值
union endian
{
int a;
char ch;
};
int main()
{
endian value;
value.a = 0x1234;
//a和ch共用4字节的内存空间
if (value.ch == 0x12)
cout << "big endian"<<endl;
else if (value.ch == 0x34)
cout << "little endian"<<endl;
}
memcpy(), strcpy(), strcmp(), strcat()都是C语言中常用的字符串处理函数,它们之间的区别如下:
memcpy(), strcpy()的区别:
功能不同:memcpy()函数用于复制一段内存区域的数据
,而strcpy()函数用于将一个字符串复制到另一个字符串中
。
参数不同:memcpy()函数需要指定要复制的内存区域的起始地址和长度
,而strcpy()函数需要指定源字符串的地址和目标字符串的地址
。
复制方式不同:memcpy()函数是按字节复制
,没有字符串的结束符’\0’的限制,可以复制任意长度的内存区域。而strcpy()函数是按字符复制
,需要遇到字符串结束符’\0’才会停止复制。
安全性不同:由于strcpy()函数没有指定目标字符串的大小,容易发生缓冲区溢出等安全问题。而memcpy()函数需要指定要复制的内存区域的长度,可以避免缓冲区溢出的问题。
总之,memcpy()和strcpy()是两个不同的函数,主要用途也不同。在实际的程序开发中,应根据具体的需求选择合适的函数,以保证代码的安全性和可靠性。如果需要复制一个字符串,应该选择strcpy()函数,如果需要复制一段内存区域,应该选择memcpy()函数。同时,为了保证代码的安全性,可以使用一些安全函数,如memcpy_s()和strcpy_s()等。
注意1:
strncpy拷贝函数并不算安全(总结:可能出现目标字符串小于源字符串的情况,会被截断,同时这种情况下不会自动添加“\0”)
:
需要手动添加字符串结束符’\0’:由于strncpy函数在复制过程中可能不会自动添加字符串结束符’\0’,因此需要手动在目标字符串的末尾添加’\0’,否则可能会导致目标字符串没有结束符,影响后续的字符串处理。
需要保证目标字符串的大小:strncpy函数在复制过程中会考虑目标字符串的大小,但需要保证目标字符串的大小足够大,以避免缓冲区溢出等问题。
需要考虑源字符串的长度:如果源字符串的长度小于n,则strncpy函数会在目标字符串的末尾添加’\0’以补齐剩余的空间。但如果源字符串的长度大于等于n,则strncpy函数只会复制前n个字符,后面的字符会被忽略。
注意2:
memcpy函数并不会在复制的数据结尾处添加任何特殊的结尾标志。所以不建议使用其复制字符串。
为了避免内存越界,需要注意以下几点:
memcpy_s(), strcpy_s(), strcmp_s()
等,它们在传递参数时需要指定缓冲区的大小,可以防止内存越界。安全函数要求在传递参数时需要指定缓冲区的大小,以防止缓冲区溢出等问题。以下是几个常见的安全函数的使用示例:
memcpy_s()函数用于复制一段内存区域的数据
,并且在参数传递时需要指定复制的字节数。它的函数原型为:errno_t memcpy_s(void* dest, size_t destSize, const void* src, size_t count)。其中,dest表示目标内存地址,destSize表示目标内存地址的大小,src表示源内存地址,count表示需要复制的字节数。
使用示例:
char str1[20] = "Hello World!";
char str2[20];
errno_t err = memcpy_s(str2, sizeof(str2), str1, sizeof(str1));
if (err == 0) {
printf("复制成功: %s\n", str2);
} else {
printf("复制失败\n");
}
strcpy_s()函数用于将一个字符串复制到另一个字符串中
,并且在参数传递时需要指定目标字符串的大小。它的函数原型为:errno_t strcpy_s(char* dest, size_t destSize, const char* src)。其中,dest表示目标字符串地址,destSize表示目标字符串的大小,src表示源字符串地址。
使用示例:
char str1[] = "Hello World!";
char str2[20];
errno_t err = strcpy_s(str2, sizeof(str2), str1);
if (err == 0) {
printf("复制成功: %s\n", str2);
} else {
printf("复制失败\n");
}
strcmp_s()函数用于比较两个字符串是否相同
,并且在参数传递时需要指定比较的字节数。它的函数原型为:errno_t strcmp_s(const char* str1, rsize_t str1max, const char* str2, int* result)。其中,str1和str2分别表示要比较的两个字符串,str1max表示比较的最大字节数,result表示比较的结果。
使用示例:
char str1[] = "Hello World!";
char str2[] = "Hello";
int result;
errno_t err = strcmp_s(str1, sizeof(str1), str2, &result);
if (err == 0) {
if (result == 0) {
printf("两个字符串相同\n");
} else {
printf("两个字符串不同\n");
}
} else {
printf("比较失败\n");
}
数组指针就是指向数组的指针,它表示的是一个指针,这个指针指向的是一个数组,它的重点是指针。例如, int(*pa)[8] 声明了一个指针,该指针指向了一个有8个int型元素的数组。下面给出一个数组指针的示例。
#include
#include
void main()
{
int b[12]={1,2,3,4,5,6,7,8,9,10,11,12};
int (*p)[4];
p = b;
printf("%d\n", **(++p);
}
程序的输出结果为 5。
上例中,p是一个数组指针,它指向一个包含有4个int类型数组的指针,刚开始p被初始化为指向数组b的首地址,++p相当于把p所指向的地址向后移动4个int所占用的空间,此时p指向数组{5,6,7,8},语句 *(++p)
; 表示的是这个数组中第一个元素的地址(可以理解p为指向二维数组的指针,{1,2,3,4},{5,6,7,8},{9,10,11,12}。p指向的就是{1,2,3,4}的地址, *p 就是指向元素{1,2,3,4}, ** p
指向的就是1,语句 **(++p)
会输出这个数组的第一个元素5。
指针数组表示的是一个数组,而数组中的元素是指针。下面给出另外一个指针数组的示例:
#include
int main()
{
int i;
int *p[4];
int a[4]={1,2,3,4};
p[0] = &a[0];
p[1] = &a[1];
p[2] = &a[2];
p[3] = &a[3];
for(i=0;i<4;i++)
printf("%d",*p[i]);
printf("\n");
return 0;
}
程序的输出结果为1234。
如果在程序中定义了一个函数,那么在编译时系统就会为这个函数代码分配一段存储空间,这段存储空间的首地址称为这个函数的地址。而且函数名表示的就是这个地址。既然是地址我们就可以定义一个指针变量来存放,这个指针变量就叫作函数指针变量,简称函数指针。
int(*p)(int, int);
这个语句就定义了一个指向函数的指针变量 p。首先它是一个指针变量,所以要有一个“*”
,即 (*p)
;其次前面的 int 表示这个指针变量可以指向返回值类型为 int 型的函数;后面括号中的两个 int 表示这个指针变量可以指向有两个参数且都是 int 型的函数。所以合起来这个语句的意思就是:定义了一个指针变量 p,该指针变量可以指向返回值类型为 int 型,且有两个整型参数的函数。p 的类型为int(*)(int,int)
。
我们看到,函数指针的定义就是将“函数声明”中的“函数名”改成“(指针变量名)”。但是这里需要注意的是:“(指针变量名)”两端的括号不能省略,括号改变了运算符的优先级。如果省略了括号,就不是定义函数指针而是一个函数声明了,即声明了一个返回值类型为指针型的函数。
最后需要注意的是,指向函数的指针变量没有 ++ 和 - - 运算。
# include
int Max(int, int); //函数声明
int main(void)
{
int(*p)(int, int); //定义一个函数指针
int a, b, c;
p = Max; //把函数Max赋给指针变量p, 使p指向Max函数
printf("please enter a and b:");
scanf("%d%d", &a, &b);
c = (*p)(a, b); //通过函数指针调用Max函数
printf("a = %d\nb = %d\nmax = %d\n", a, b, c);
return 0;
}
int Max(int x, int y) //定义Max函数
{
int z;
if (x > y)
{
z = x;
}
else
{
z = y;
}
return z;
}
首先它是一个函数,只不过这个函数的返回值是一个地址值。函数返回值必须用同类型的指针变量来接受,也就是说,指针函数一定有“函数返回值”,而且,在主调函数中,函数返回值必须赋给同类型的指针变量。
类型名 *函数名(函数参数列表);
其中,后缀运算符括号“()”表示这是一个函数,其前缀运算符星号“*”表示此函数为指针型函数,其函数值为指针,即它带回来的值的类型为指针,当调用这个函数后,将得到一个“指向返回值为…的指针(地址),“类型名”表示函数返回的指针指向的类型”。
“(函数参数列表)”中的括号为函数调用运算符,在调用语句中,即使函数不带参数,其参数表的一对括号也不能省略。其示例如下:
int *pfun(int, int);
由于“*”的优先级低于“()”的优先级,因而pfun首先和后面的“()”结合,也就意味着,pfun是一个函数。
即:
int *(pfun(int, int));
接着再和前面的“*”结合,说明这个函数的返回值是一个指针。由于前面还有一个int,也就是说,pfun是一个返回值为整型指针的函数。
#include
float *find(float(*pionter)[4],int n);//函数声明
int main(void)
{
static float score[][4]={{60,70,80,90},{56,89,34,45},{34,23,56,45}};
float *p;
int i,m;
printf("Enter the number to be found:");
scanf("%d",&m);
printf("the score of NO.%d are:\n",m);
p=find(score,m-1);
for(i=0;i<4;i++)
printf("%5.2f\t",*(p+i));
return 0;
}
float *find(float(*pionter)[4],int n)/*定义指针函数*/
{
float *pt;
pt=*(pionter+n);
return(pt);
}
共有三个学生的成绩,函数find()被定义为指针函数,其形参pointer是指针指向包含4个元素的一维数组的指针变量。pointer+n指向score的第n+1行。*(pointer+1)指向第一行的第0个元素。pt是一个指针变量,它指向浮点型变量。main()函数中调用find()函数,将score数组的首地址传给pointer。
相同
#include "stdio.h"
int main(){
int x = 5;
int *p = &x;
int &q = x;
printf("%d %d\n",*p,sizeof(p));
printf("%d %d\n",q,sizeof(q));
}
//结果
5 8
5 4
由结果可知,引用使用时无需解引用(*),指针需要解引用;用的是64位操作系统,“sizeof 指针”得到的是指针本身的大小,即8个字节。而“sizeof 引用”得到的是的对象本身的大小及int的大小,4个字节。
int a = 5;
int *p = &a;
void fun(int &x){}//此时调用fun可使用 : fun(*p);
//p是指针,加个*号后可以转换成该指针指向的对象,此时fun的形参是一个引用值,
//p指针指向的对象会转换成引用X。
//将指针初始化为NULL。
char * p = NULL;
//用malloc分配内存
char * p = (char * )malloc(sizeof(char));
//用已有合法的可访问的内存地址对指针初始化
char num[ 30] = {0};
char *p = num;
delete(p);
p = NULL;
注:malloc函数分配完内存后需注意:
a. 检查是否分配成功(若分配成功,返回内存的首地址;分配不成功,返回NULL。可以通过if语句来判断)
b. 清空内存中的数据(malloc分配的空间里可能存在垃圾值,用memset或bzero 函数清空内存)
//s是需要置零的空间的起始地址; n是需要置零的数据字节个数。
void bzero(void *s, int n);
// 如果要清空空间的首地址为p,value为值,size为字节数。
void memset(void *start, int value, int size);
为了避免野指针的出现,我们可以采取以下几个方法:
使用智能指针:智能指针是 C++11 引入的一种 RAII 技术,可以自动管理内存资源,避免手动管理内存带来的风险。C++ STL 中提供了三种智能指针:unique_ptr
、shared_ptr
和 weak_ptr
。其中,unique_ptr
用于管理独占的资源,shared_ptr
用于管理共享的资源,weak_ptr
用于解决循环引用问题。
使用容器类:C++ STL 中提供了各种容器类,例如 vector
、list
、map
等,可以避免手动管理内存带来的风险。容器类可以自动管理内存,提供了安全的访问和操作接口。
避免手动管理内存:手动管理内存容易出现各种错误,例如忘记释放、重复释放、野指针等。我们可以使用 C++ STL 中的容器类、智能指针等 RAII 技术来管理内存,从而避免手动管理内存带来的风险。
使用工具检测野指针:可以使用一些工具来检测野指针的存在,例如内存泄漏检测工具、静态代码分析工具等。
数组的优点:
链表的优点:
预处理指令
,在预处理时进行简单而机械的字符串替换,不做正确性检査
,不管含义是否正确照样代入,只有在编译已被展开的源程序时,才会发现可能的错误并报错。# define Pl3.1415926
,当程序执行 area=Pr*r
语句时,PI会被替换为3.1415926。于是该语句被替area=3.1415926*r*r
。如果把# define语句中的数字9写成了g,预处理也照样代入,而不去检查其是否合理、合法。编译时处理
,所以 typedef具有类型检查的功能。它在自己的作用域内给一个已经存在的类型一个别名,但是不能在一个函数定义里面使用标识符 typedef。例如, typedef int INTEGER ,这以后就可用 INTEGER来代替int作整型变量的类型说明了,例如: INTEGER a,b; typedef int a[10];
表示a是整型数组类型,数组长度为10。然后就可用a说明变量,例如:语句a s1,s2;完全等效于语句 int s1[10],s2[10].同理, typedef void(*p)(void)
表示p是一种指向void型的指针类型。 typedef int (*PF)(const char *, const char*)
typedef long double REAL
,在不支持 long double的机器上,该 typedef看起来会是下面这样: typedef double real
,在 double都不支持的机器上,该 typedef看起来会是这样: typedef float REAL 。#define不只是可以为类型取别名,还可以定义常量、变量、编译开关等。#define没有作用域的限制,只要是之前预定义过的宏,在以后的程序中都可以使用,而 typedef有自己的作用域。
void fun()
{
#define A int
}
void gun()
{
//这里也可以使用A,因为宏替换没有作用域,但如果上面用的是 typedef,那这里就不能用
//A,不过,一般不在函数内使用 typedef
}
两者修饰指针类型时,作用不同。
#define INTPTR1 int*
typedef int* INTPTR2;
INTPTR1 pl, p2;
INTPTR2 p3, p4;
INTPTR1 pl, p2和INTPTR2 p3, p4的效果截然不同。 INTPTR1 pl, p2进行字符串替换后变成 int*p1,p2
,要表达的意义是声明一个指针变量p1和一个整型变量p2.而 INTPTR2 p3, p4,由于INTPTR2是具有含义的,告诉我们是一个指向整型数据的指针,那么p3和p4都为指针变量,这句相当于int*pl,*p2
.从这里可以看出,进行宏替换是不含任何意义的替换,仅仅为字符串替换;而用 typedef为一种数据类型起的别名是带有一定含义的。
程序示例如下
#define INTPTR1 int*
typedef int* INTPTR2
int a=1;
int b=2;
int c=3;
const INTPTR1 p1=&a;
const INTPTR2 p2=&b;
INTPTR2 const p3=&c;
上述代码中, const INTPTR1 p1表示p1是一个常量指针,即不可以通过p1去修改p1指向的内容,但是p1可以指向其他内容。而对于 const INTPTR2 p2,由于 INTPTR2表示的是个指针类型,因此用 const去限定,表示封锁了这个指针类型,因此p2是一个指针常量,不可使p2再指向其他内容,但可以通过p2修改其当前指向的内容。 INTPTR2 const p3同样声明的是一个指针常量。
对于 include< filename. h>,编译器先从标准库路径开始搜索 filename.h,使得系统文件调用较快。
而对于# include“ filename.h"”,编译器先从用户的工作路径开始搜索 filename.h,然后去寻找系统路径,使得自定义文件较快。
//宏定义多层嵌套(10 * 10 * 10),printf多次输出。
#include
#define B P,P,P,P,P,P,P,P,P,P
#define P L,L,L,L,L,L,L,L,L,L
#define L I,I,I,I,I,I,I,I,I,I,N
#define I printf("%3d",i++)
#define N printf("n")
int main()
{
int i = 1;
B;
return 0;
}
//简便写法,同样使用多层嵌套
#include
#define A(x)x;x;x;x;x;x;x;x;x;
int main ()
{
int n=1;
A(A(A(printf("%d", n++);
return 0;
}
一个源程序到一个可执行程序的过程:预编译、编译、汇编、链接
。
attribute可以设置函数属性(Function Attribute)、变量属性(Variable Attribute)和类型属性(Type Attribute)。
gnu对于函数属性主要设置的关键字如下:
alias: 设置函数别名。
aligned: 设置函数对齐方式。
always_inline/gnu_inline:
函数是否是内联函数。
constructor/destructor:
主函数执行之前、之后执行的函数。
format:
指定变参函数的格式输入字符串所在函数位置以及对应格式输出的位置。
noreturn:
指定这个函数没有返回值。
请注意,这里的没有返回值,并不是返回值是void。而是像_exit/exit/abord那样执行完函数之后进程就结束的函数。
weak:指定函数属性为弱属性,而不是全局属性,一旦全局函数名称和指定的函数名称命名有冲突,使用全局函数名称。
示例代码如下:
#include
void before() __attribute__((constructor));
void after() __attribute__((destructor));
void before() {
printf("this is function %s\n",__func__);
return;
}
void after(){
printf("this is function %s\n",__func__);
return;
}
int main(){
printf("this is function %s\n",__func__);
return 0;
}
// 输出结果
// this is function before
// this is function main
// this is function after
大多数CPU上的程序实现使用栈来支持函数调用操作,栈被用来传递函数参数、存储返回信息、临时保存寄存器原有的值以备恢复以及用来存储局部变量。
函数调用操作所使用的栈部分叫做栈帧结构,每个函数调用都有属于自己的栈帧结构,栈帧结构由两个指针指定,帧指针(指向起始),栈指针(指向栈顶),函数对大多数数据的访问都是基于帧指针。下面是结构图:
栈指针和帧指针一般都有专门的寄存器,通常使用ebp寄存器作为帧指针,使用esp寄存器做栈指针。
帧指针指向栈帧结构的头,存放着上一个栈帧的头部地址,栈指针指向栈顶。
在C语言中,如果一些函数被频繁调用,不断地有函数入栈,即函数栈,会造成栈空间或栈内存的大量消耗。为了解决这个问题,特别的引入了inline修饰符,表示为内联函数
。
大多数的机器上,调用函数都要做很多工作:调用前要先保存寄存器,并在返回时恢复,复制实参,程序还必须转向一个新位置执行C++中支持内联函数,其目的是为了提高函数的执行效率,用关键字 inline放在函数定义(注意是定义而非声明)的前面即可将函数指定为内联函数,内联函数通常就是将它在程序中的每个调用点上“内联地”展开。
内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率
。
内联函数的主要特点包括:
inline int add(int a, int b) {
return a + b;
}
在上面的代码中,add 函数被定义为内联函数,这意味着在函数调用处,编译器会将函数体直接展开,而不是通过函数调用的方式执行。这样可以避免函数调用的开销,提高程序的运行速度。
析构函数必须是虚函数的原因是为了正确地销毁对象和避免内存泄漏。
当一个对象被销毁时,会先调用它的析构函数,然后再释放它所占用的内存。如果一个类有继承关系,而它的析构函数不是虚函数,当使用基类指针指向派生类对象并销毁这个对象时,只会调用基类的析构函数,而不会调用派生类的析构函数,导致派生类对象中的资源无法正确释放,从而引发内存泄漏。
例如,假设有一个基类 Animal
和它的一个派生类 Cat
,并且它们都有一个需要释放的资源 m_pData
,代码如下所示:
class Animal {
public:
Animal() { m_pData = new int(0); }
~Animal() { delete m_pData; }
private:
int* m_pData;
};
class Cat : public Animal {
public:
Cat() { m_pData = new int(1); }
~Cat() { delete m_pData; }
private:
int* m_pData;
};
如果将析构函数改为非虚函数,那么当使用基类指针指向派生类对象并销毁这个对象时,只会调用基类的析构函数,而不会调用派生类的析构函数,导致派生类对象中的 m_pData
资源无法正确释放,从而引发内存泄漏。
C++默认的析构函数不是虚函数是因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。因此C++默认的析构函数不是虚函数,而是只有当需要当作父类时,设置为虚函数。
如果构造函数打开了一个文件,最后不需要使用时文件就要被关闭。析构函数允许类自动完成类似清理工作,不必调用其他成员函数。
析构函数也是特殊的类成员函数。简单来说,析构函数与构造函数的作用正好相反,它用来完成对象被删除前的一些清理工作,也就是专门的扫尾工作。
静态函数在编译的时候就已经确定运行时机,虚函数在运行的时候动态绑定。虚函数因为用了虚函数表机制,调用的时候会增加一次内存开销。
原理:
虚函数表是一个类的虚函数的地址表,每个对象在创建时,都会有一个指针指向该类虚函数表,每一个类的虚函数表,按照函数声明的顺序,会将函数地址存在虚函数表中,当子类对象重写父类的虚函数的时候,父类的虚函数表中对应的位置会被子类的虚函数地址覆盖。
作用:
在用父类的指针调用子类对象成员函数时,虚函数表会指明要调用的具体函数是哪个。
重载(Overload)和覆盖(Override)是两个不同的概念,它们的区别主要体现在以下几个方面:
程序代码如下:
#include
int func(int x)
{
int countx = 0;
while(x)
{
countx++;
x = x&(x-1);
}
return countx;
}
int main()
{
printf("%d\n",func(9999));
return 0;
}
程序输出的结果为8。
在上例中,函数func()的功能是将x转化为二进制数,然后计算该二进制数中含有的1的个数。首先以9为例来分析,9的二进制表示为1001,8的二进制表示为1000,两者执行&操作之后结果为1000,此时1000再与0111(7的二进制位)执行&操作之后结果为0。
为了理解这个算法的核心,需要理解以下两个操作:
1)当一个数被减1时,它最右边的那个值为1的bit将变为0,同时其右边的所有的bit都会变成1。
2)每次执行x&(x-1)的作用是把ⅹ对应的二进制数中的最后一位1去掉。因此,循环执行这个操作直到ⅹ等于0的时候,循环的次数就是x对应的二进制数中1的个数。
int CountZeroBit(int num)
{
int count = 0;
while (num + 1)
{
count++;
num |= (num + 1); //算法转换
}
return count;
}
int main()
{
int value = 25;
int ret = CountZeroBit(value);
printf("%d的二进制位中0的个数为%d\n",value, ret);
system("pause");
return 0;
}
有两种解法, 一种用算术算法, 一种用^(异或)。
方法一:
a = a + b;
b = a - b;
a = a - b;
方法二:
a = a^b;// 只能对int,char..
b = a^b;
a = a^b;
or
a ^= b ^= a;
给定一个整型变量a,写两段代码,第一个设置a的bit 3,第二个清除a 的bit 3。在
以上两个操作中,要保持其它位不变。
#define BIT3 (0x1<<3)
static int a;
void set_bit3(void)
{
a |= BIT3;
}
void clear_bit3(void)
{
a &= ~BIT3;
}