Linux兴趣小组2017-2020年面试题总结

感谢高学长给予的帮助和解惑

1.sizeof和strlen的应用

这应该是一个比较问的多的问题,我觉得一开始学习这两个形似(都是测大小)而神不似(测的实质不一样)的运算都是从只读懂了概念开始的,所以我先强调一下它们的定义以及用法

sizeof运算符以字节为单位,可以以类型,函数做参数,返回其操作数的大小,即size_t类型的值,这是一个无符号整型,在头文件中被typedef定义到unsigned int,它的功能是

获得保证能容纳实现所建立的最大对象字节大小

记住

  • sizeof 在计算变量所占空间大小时,括号可以省略,而计算类型(模子)大小时括号不能省略
  • sizeof 后面的内容在编译时就已经完成了,之后再运算并替换为运算后的数值,在运行程序时直接调用替换值
  • sizeof (数组名),这个数组名代表整个数组,它测的整个数组大小
strlen是一个函数,只能以字符串做参数,而且想要得到结果必须字符串里包含’\0’,它的功能是

计算字符串空间个数但不包括’\0’

下面分析一道程序的输出.

int main(int argc, char *argv[])
{
    int t = 4;
    printf("%lu\n", sizeof(t--));
    printf("%lu\n", sizeof("ab c\nt\012\xa1*2"));
    return 0;
}
  1. 所以printf("%lu\n", sizeof(t–))值为4(这里的4是int型的字节大小),至于t的值是否改变了,可以printf("%d\n",t);前后看一看
  2. ’\n’,’\012’,’\xa1’ 分别是一个字符回车,8进制的012,16进制的a1,且计算时包含结束符’\0’

分析以下代码段,给出输出结果,并给出自己的理解。

int main(int argc, char *argv[])
{
	int a[3][2] = { 2, 0, 1, 8 };
	char *str = (char *)malloc(sizeof(char) * 20);
	printf("%d\n",nums[1][-2]););
	strcpy(str, "\0101\\xb2");
	printf("%zu\n", sizeof(a));
	printf("%zu %d\n", sizeof(a[1][1] =0), a[1][1]);
	printf("%zu %zu\n", sizeof(str), strlen(str));
}
  1. 这题跟上面一道差不多,只不过sizeof(a)这个二维数组的大小就是(3*2)6 * 4=24个字节
  2. 这里便证明了上面所说的结论,sizeof 是在编译阶段已经计算出来结果,而赋值是在运算过程中的,所以a[1][1]=0不会执行
  3. 这里字符指针变量str用malloc动态开辟出来了一片空间有20个字节大小,再用strcpy过去一个字符串,其中**’\010’,’’,’\x’** 分别为单独的字符,所以sizeof()计算的是 str 这个指针变量的长度,而strlen()函数计算的则是str指向字符串的有效长度。

2.数组与指针

分析以下代码段,并解释输出结果和原因。

int main(int argc, char *argv[])
{
	int nums[3][3] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
	printf("%d\n", nums[1][-2] );
	printf("%d\n", (-1)[nums][5] );
	printf("%d\n", -1[nums][5] );
}

对于数组我们要有一个认识就是数组里面的 [ ] 变址运算符,就相当于*(+)的意义
其次不管一维二维甚至三维数组,它们实际在硬件存储器里都是按一维编址线性排列的,并且c语言规定按行优先存放

  1. 所以nums[1][-2]可以写成*(*(nums+1)-2),即在+1(加上一个一维数组的字节(3个int类型大小))的基础上减去2个int类型大小,简单来说可以看成nums[0][1]
  2. 同理(-1)nums[5]=nums[-1][5]=nums[0][2]
  3. -1[nums][5]=-(1)nums[5]=-nums[1][5]=-nums[2][2]=-9

下列程序分别输出的是数组中的第几个 0 ?

int main(int argc, char *argv[])
{
	int a[][2] = {0, 0, 0, 0, 0, 0, 0, 0};
	for(int i = 0; i <= 2; i++)
	{
	 	printf("%d\n", a[i][i]);
	}
}
  1. 首先我们先把二维数组a[ ][2]给补充完整是
    0        0       0       0       0      0        0      0
a[0][0]  a[0][1] a[1][0] a[1][1] a[2][0] a[2][1] a[3][0] a[3][1]
  1. 然后我们看这个循环可以得到a[0][0],a[1][1],a[2][2]
  2. 咦,a[2][2]这道题有问题??,我们要始终坚定数组是线性排列的,所谓二维数组只是便于理解的一种写法,a[2][2]的实质其实只是a[3][0]

以下程序运行结果是什么?

int main(int argc, char *argv[])
{
	int nums[5] = {2, 4, 6, 8, 10};
	int *ptr = (int *)(&nums+1);
	printf("%d, %d\n", *(nums+1), *(ptr-1));
}
  1. 数组nums的地址是一个数组指针类型,即nums内存中有5个int类型的值,&nums后指向了数组nums的开头,&nums+1后跳过了这个数组指向下一个数组(这里我们可以理解为指向了nums的结尾即5的地址),然后把int类型的值强转为指针变量并用ptr指向它们。
  2. 所以(nums+1)的意思就是a[1],即第二个元素的地址解引用
  3. *(ptr-1)的意思就是ptr指向的nums数组里面最后一个元素类型向后移动一位得到它的地址解引用
  4. &nums类型是int(*)[5]类型,即数组指针类型,要强制转换一下才可以

下面代码使用正确吗?若正确,请说明代码的作用;
若不正确,请指出错误并修改。

void get_str(char *ptr)
{
 	ptr = (char*)malloc(17);
 	strcpy(ptr, "Xiyou Linux Group");
}
int main(int argc, char *argv[])
{
 	char *str = NULL;
 	get_str(str);
 	printf("%s\n", str);
}
  1. 主函数中的指针变量str首先指向空指针,
  2. 在作为参数传进函数 get_str 时传给 ptr 时, 在被调函数中仅仅将 ptr 指向的内存地址变为了malloc函数申请的空间, 而主函数中的str仍指向空指针,并不会输出字符.
  3. malloc函数应该开辟18个字节的空间,因为字符串结尾会自动补‘\0’,加上它一共18字节。
  4. “值传递”为单向传递,形参和实参各占不同的内存单元,形参值无法传回实参,不能实现复制并输出字符串的功能。所以可以使用二级指针,“址传递”实现双向传递。

3.数组与地址

猜想下面程序的输出,并谈谈自己的理解

int main(int argc, char *argv[])
{
	int a[5];
	printf("%p\n", a);
	printf("%p\n", a+1);
	printf("%p\n", &a);
	printf("%p\n", &a+1);
}
  1. a与&a取的都是数组首地址,即a[0]的地址
  2. a+1取的是a[1]的地址,即在a[0]基础上加上一个int型大小
  3. &a+1中的&a代表整个数组+1后就是跳过这个数组指向下一个数组,即加上5个int型大小

执行下面的程序段,每次执行的输出结果一致吗,整理并解释输出结果。

int main(int argc, char *argv[]) 
{
 	int a[4] = { 2, 0, 1, 9 };
 	printf("%p, %p\n", a, &a);
 	printf("%p, %p\n", a + 1, &a + 1);
 }
  1. 同上

4.static的作用

第一个作用作为变量,变量又分为局部和全局变量,但它们都存在内存的静态区。

static局部变量:在函数体里面定义的,就只能在这个函数里用了,同一个文档中的其他函数也用不了。由于被 static 修饰的变量总是存在内存的静态区,所以即使这个函数运行结束,这个静态变量的值还是不会被销毁,函数下次使用时仍然能用到这个值,普通局部变量:存储于栈中,使用完毕会立即释放
static全局变量:作用域仅限于变量被定义的文件中,其他文件即使用 extern 声明也没法使用它。普通全局变量:其他文件可以使用extern外部声明后直接使用。也就是说其他文件不能再定义一个与其相同名字的变量了(否则编译器会认为它们是同一个变量)。
static全局变量只初始化一次,防止在其他文件单元被引用。普通全局变量和它都在静态区被初始化,但普通被调用一次就会初始化一次

第二个作用修饰函数

static静态函数:使函数的作用域仅局限于本文件(所以又称内部函数)。
static函数在内存中只有一份,普通函数在每个被调用中维持一份拷贝

参考所给代码,简要地谈一谈你对 static 关键字的理解。

static int a = 2018;
static void func(void)
{
	static int b;
	printf("a = %d, b = %d\n", a++, ++b);
}
int main(int argc, char *argv[])
{
	func( );
	func( );
	func( );
}

有如下代码段所示的函数 f,当我们执行该函数时,会产生什么样的输出结果?在同一程序中多次执行该函数,输出结果是否一致?

void f() 
{
	static int a = 0;
 	int b = 0;
 	printf("%d, %d\n", ++a, ++b);
 }

这里可以很明显看出a是静态局部变量,其值不会被销毁在一直被增加,而b的值使用一次就销毁一次

5.const的作用

const 关键字的作用是什么?下面的这几种定义有区别吗?

const char *p;//不能改变p指向的值,即该地址存储的值,但可以指为其它的地址
char const *p;//同上
char *const p;//必须指向同一个地址,但所指向的值可以改变
const char *const p;//必须指向同一位置,并且它所指向位置存储的值不能变

根据所给代码,说明 const 关键字的用法,指出标号为 (1)~(4) 的代码哪些是错误的。

char y[ ] = "XiyouLinuxGroup",x[ ] = "2018";
char *const p1 = y;//p1(地址)不可变,p1指向的对象(值)可变
const char *p2 = y;//p2(地址)可变,p2指向的对象(值)不可变
/* (1) */ p1 = x;//错误
/* (2) */ p2 = x;//正确
/* (3) */ *p1 = 'x';//正确
/* (4) */ *p2 = 'x';//错误

简而言之

const int *p; //const 修饰*p,p 是指针,*p 是指针指向的对象,不可变
int const *p; //const 修饰*p,p 是指针,*p 是指针指向的对象,不可变
int *const p; //const 修饰 p,p 不可变,p 指向的对象可变
const int *const p; //前一个 const 修饰*p,后一个 const 修饰 p,指针 p 和 p 指向的对象都不可变

另外
编译器通常不为普通 const 只读变量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的值,没有读内存与存储的操作,它们的效率也很高
补充一点const与#define的区别
1.const 定义的只读变量从汇编的角度来看,只是给出了对应的内存地址,而不是像#define一样给出的是立即数
2.const 定义的只读变量在程序运行过程中只有一份拷贝(因为
它是全局的只读变量,存放在静态区),而#define 定义的宏常量在内存中有若干个拷贝

3.#define 宏是在预编译阶段进行替换,而 const 修饰的只读变量是在编译的时候确定其值
4.#define 宏没有类型,而 const 修饰的只读变量具有特定的类型
说到了这里我们再来看看#define宏的用处了

6.#define宏定义参数

#define 替换规则
在程序中扩展#define定义符号和宏时,需要涉及几个步骤。

  1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
  2. 替换文本随后被插入到程序中原来文本的位置.对于宏,参数名被他们的值替换。
  3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
    注意:
  4. 宏参数和#define 定义中可以出现其他#define定义的变量。但是对于宏,不能出现递归。
  5. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。

说说 #include<> 和 #include" " 有什么区别?为什么需要使用 #include?
#include" " 查找策略
先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头(即 C 编译系统所提供的并存放在指定的子目录下的头 文件文件一样在标准位置查找头文件。 如果找不到就提示编译错误。 linux环境的标准头文件的路径:/usr/include
#include<> 查找策略:
查找头文件直接去标准系统目录下去查找(即C编译系统所提供的并存放在指定的子目录下的头文件),如果找不到就提示编译错误
#include实质:
预编译时将后面文件的内容包含到当前文件中,即是将已存在文件的内容嵌入到当前文件中。
分析下面这段代码,说出最终输出结果。

#define NAME(n) x##n
#define PRINTVAL(y, ...) printf("x"#y":" __VA_ARGS__)
int main(int argc, char *argv[])
{
	int NAME(1);
	short *NAME(2) = ( short *)&NAME(1);
	char *NAME(3) = (char *)&NAME(1);
	NAME(1) = 0; 
	*NAME(2) = -1;
	PRINTVAL(1, "%x\n", NAME(1));
	PRINTVAL(2, "%x\n", *NAME(2));
	PRINTVAL(3, "%x\n", *NAME(3));
}
  1. 字符串是有自动连接的特点的
  2. 只有当字符串作为宏参数的时候才可以把字符串放在字符串中
  3. 使用 # ,可以把一个宏参数变成对应的字符串
  4. 使用 ## ,可以把位于它两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符
  5. 可变宏: …和 _VA_ARGS _可用在替换部分中,表示省略号代表什么

这段代码实际就是

int main(int argc,char *argv[]) {
	int x1;
 	short *x2 = (short *)&x1;
 	char *x3 = (char *)&x2;
	x1 = 0; 
	*x2 = -1;
	printf("x""1"":" "%x\n",x1);//ffff
	printf("x""2"":" "%x\n",*x2);//ffffffff
	printf("x""3"":" "%x\n",*x3);//ffffffff
	}
  1. 一个十六进制的f等于1111,-1的十六进制就是ffffffff
  2. 强转后x2为short类型指针,占前面两个地址,即0x0000 0x0001,即为0xffff,%x读出四个字节,补两个字节,添加到最高位
  3. 强转后x3为char类型指针,占第一个的地址,即0x0000,补三个字节
  4. x1依旧是int类型的变量,用小端模式读出为0x0000ffff
0x0000 0x0001 0x0002 0x0003
 ff   ff     00      00

分析以下代码段,解释输出的结果。

#define YEAR 2018
#define LEVELONE(x) "XiyouLinux "#x"\n" 
#define LEVELTWO(x) LEVELONE(x)
#define MULTIPLY(x,y) x*y
int main(int argc, char *argv[])
{
	int x = MULTIPLY(1 + 2, 3);
	printf("%d\n", x);
	printf(LEVELONE(YEAR));
	printf(LEVELTWO(YEAR));
}
  1. 嵌套宏的展开规律:
    一般展开规律为:先展开参数,再分析函数,由内向外展开
    当宏中有#运算符的时候,不展开参数
    当宏中有##运算符的时候,先展开函数,再分析参数
    ##运算符用于将参数连接到一起,预处理过程把出现在##运算符两侧的参数合并成一个符号,注意不是字符串

下面代码段的输出结果是什么?输出该结果的原因是?

#define X a+b
int main(int argc, char *argv[]) 
{
 	int a = 1, b = 1;
 	printf("%d\n", X*X);
}
  1. a+b*a+b=3

请说出 a 在不同平台上的值,并思考此种方式的优点。

#ifdef __linux__
	int a = 1;
#elif _WIN32
	int a = 2;
#elif __APPLE__
	int a = 3;
#else
	int a = 4;
#endif

便于移值

7.无符号整型与有符号整型运算

下面的代码输出什么 ? 为什么 ?

int main(int argc, char *argv[])
{ 
	unsigned int a = 10;
	int b = -20;
 	if (a + b > 0)
 	printf("a+b = %d\n", a+b);
 else
 	printf("a = %d b = %d\n", a, b);
 }
  1. 隐式类型转换:
    执行算术运算时,低类型(短字节)可以转换为高类型(长字节);例如: int型转换成double型,int转为unsigned int型,char型转换成int型等等;
    赋值表达式中,等号右边表达式的值的类型自动隐式地转换为左边变量的类型,并赋值给它;
    函数调用时,将实参的值传递给形参,系统首先会自动隐式地把实参的值的类型转换为形参的类型,然后再赋值给形参;
    函数有返回值时,系统首先会自动隐式地将返回表达式的值的类型转换为函数的返回类型,然后再赋值给调用函数返回;
    综上,这种转换称为整型提升
  2. 整型提升的意义
    表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度,同时也是CPU的通用寄存器的长度。
    因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。
    通用CPU是难以直接实现两个8比特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转换为int或unsigned int,然后才能送到cpu去执行运算

举个粟子
相加的时候 ,有符号会自动转化为无符号之后在进行运算

整形提升是按照变量的数据类型的符号位来提升的

//负数的整形提升
char c1 = -1;
变量c1的二进制位(补码)中只有8个比特位:
1111111
因为 char 为有符号的 char
所以整形提升的时候,高位补充符号位,即为1
提升之后的结果是:
11111111111111111111111111111111
//正数的整形提升
char c2 = 1;
变量c2的二进制位(补码)中只有8个比特位:
00000001
因为 char 为有符号的 char
所以整形提升的时候,高位补充符号位,即为0
提升之后的结果是:
00000000000000000000000000000001
//无符号整形提升,高位补0
  1. 只要参与表达式运算,就会发生整形提升
int main()
{
 	char c = 1;
 	printf("%u\n", sizeof(c));//1
 	printf("%u\n", sizeof(+c));//4
 	printf("%u\n", sizeof(!c));//4
}
  1. 回到这题我们知道当unsigned intint进行相加的时候 ,有符号会自动转化为无符号之后在进行运算,而此时int型的变量整型提升后为unsigned int型,无符号数的补码最高位不表示符号,所以一个负数表示为无符号数的时候他就会变成一个很大的正数所以a+b>0,打印时%d则按有符号输出.
  2. 注:不想转换为二进制的话,可以 -20 + 2^32 ,这样就转换为unsigned int 了

下面代码段将打印出多少个‘=’?运用相关知识解释该输出。

int main(int argc, char *argv[])
{
 	for (unsigned int i = 3; i >= 0; i--)
 	putchar('=');
 }
  1. 隐式转换,整型提升

执行下面的代码段,会输出什么?请试着解释其原因,并叙述相关知识。

int main(int argc, char *argv[]) 
{
 	char ch = 255;
 	//unsigned ch=255;
 	int d = ch + 1;
 	printf("%d %d", ch, d);
}
  1. 简单的来说就是当char和unsigned char都要强制转换成int类型(虽然正常也不会直接把unsigned char转换成int),这个时候如果两个最高位都是0,就都没问题。但是如果最高位都为1,那么char转换成int是没问题。但是unsigned char转换就会把最高位的1当做符号位。最高位若为0时,二者没有区别
  2. 在C中,默认的基础数据类型均为signed
  3. unsigned char在内存上的存储是11111111,(signed) char在内存上的存储是10000001
  4. unsigned char+1就是256了,相当于
  1111 1111
+ 0000 0001
=10000 0000(256)
  1. signed char+1就是-1+1,取后八位上溢的0, 然后被提升成了32位0,输出0

8.位操作

分析以下函数,给出 f(2018) 的值,推测并验证函数的作用。

int f(unsigned int num)
{
	for (unsigned int i = 0; num; i++)
		num &= (num - 1);
	return i;
}
  1. & 运算符,两个数对应的二进制位都为1结果为1 ,否则 为 0
  2. 第一次循环num=2018&2017等价于11111100010&11111100001 结果为11111100000,即 num = 2016
  3. 第二次循环num=2016&2015等价于11111100000 & 11111011111 结果为 11111000000,即num=1984
  4. 第三次循环num=1984&1983等价于11111000000&11110111111结果为11110000000,即num=1920
  5. num&=(num-1)目的是去掉二进制末尾的1,可以用来计算一个数二进制1的个数
int count_one(int num)
{
	int count = 0;  //记录1的个数
	while (num)
	{
		++count;
		num &= (num - 1);  
	}
	return count;
}
  1. 依次类推直到num为0时退出循环
  2. 个人想法,直接看2018的二进制有几个1就行了

下列三种交换整数的方式是如何实现交换的?

/* (1) */ int c = a; a = b ; b = c;
/* (2) */ a = a - b; b = b + a; a = b - a;
/* (3) */ a ^= b ; b ^= a ; a ^= b;
  1. 这是一个思维问题
a=a-b;a=a0-b0  b=b0
b=b+a;b=a0  a=a0-b0
a=b-a;a=b0  b=a0
  1. 但是因为加减法需要先将操作数复制到寄存器(eax)里,再做加减法运算,显而易见的漏洞在于如果a+b的值大于数据型的上限的话会导致溢出,于是为了不溢出,出现了第三种方法,通过异或运算防止溢出
  2. **^**位异或的用法是只有一个为真时为真

执行以下代码段,将产生什么样的输出?请对输出加以解释,并手动计算代码中 t 的值。

int main(int argc, char *argv[])
{
 	char x = -2, y = 3;
 	char t = (++x) | (y++);
 	printf("x = %d, y = %d, t = %d\n", x, y, t);
 	t= (++x) || (y++);
 	printf("x = %d, y = %d, t = %d\n", x, y, t);
 }
  1. **|**位或的用法是如果任意一个为真时为真
  2. t=-1 | 3,-1的二进制为1111 1111 1111 1111 1111 1111 1111 1111,位操作后t=-1,y=4
  3. **||**逻辑或的用法是如果左边为真,右边就不看了
  4. **&&**逻辑与的用法是如果左边为假,右边就不看了
  5. t=0 || 4,左边为假,右边为真,结果为真,y++会进行运算
  6. 贴一个补充吧
int main()
{
    int i = 0,a=0,b=2,c =3,d=4;
    i = a++ && ++b && d++;
    //i = a++||++b||d++;
    printf("a = %d\n b = %d\n c = %d\nd = %d\n", a, b, c, d);
}
//程序输出的结果是什么?

9.大端小端

  1. 什么是大端小端
    大端(存储)模式,是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中;
    小端(存储)模式,是指数据的低位保存在内存的低地址中,而数据的高位,,保存在内存的高地址中
  2. 为什么有大端小端:
    为什么会有大小端模式之分呢?这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为8bit。但是在C语言中除了8bit的char之外,还有16bit的short型,32bit的long型(要看具体的编译器),另外,对于位数大于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如果将多个字节安排的问题。因此就导致了大端存储模式和小端存储模式。
    例如一个 16bit 的 short 型 x ,在内存中的地址为 0x0010 , x 的值为 0x1122 ,那么 0x11 为高字节, 0x22为低字节。对于大端模式就将 0x11 放在低地址中,即 0x0010 中0x22 放在高地址中,即 0x0011 中。小
    端模式,刚好相反。我们常用的 X86 结构是小端模式,而 KEIL C51则为大端模式。很多的ARM,DSP都为小端模式。有些ARM处理器还可以由硬件来选择是大端模式还是小端模式

观察程序的运行结果,说明出现这种情况的原因。

int main(int argc, char *argv[])
{
 	char str[512];
	int i;
 	for (i = 0; i < 512; ++i)
 		str[i] = -1 - i;
 	printf("%lu\n", strlen(str));
 }
  1. -1 的补码为 0xff,即1111 1111 1111 1111 1111 1111 1111 1111-2 的补码为 0xfe……当 i 的值为 127时,a[127]的值为-128,而-128 是 char 类型数据能表示的最小的负数。当 i 继续增加,a[128]的值肯定不能是-129。因为这时候发生了溢出,-129 需要 9 位才能存储下来,而 char 类型数据只有 8 位,所以最高位被丢弃。剩下的 8 位是原来 9 位补码的低 8 位的值,即 0x7f。当 i 继续增加到 255 的时候,str[255]=-1-255=-256,补码1111 1111 1111 1111 1111 1111 1111 1111-0000 0000 0000 0000 0000 0000 1111 1111=1111 1111 1111 1111 1111 1111 0000 0000,结果str[255]=(补码0000 0000)为0,因为存进char类型,所以变为‘\0’结束符号,所以最后strlen结果为255
  2. 然后当 i 增加到 256 时,-257 的补码的低 8 位全为 1,即低八位的补码为 0xff,如此又开始一轮新的循环……按照上面的分析,a[0]到 a[254]里面的值都不为 0,而 a[255]的值为 0。strlen 函数是计算字符串长度的,并不包含字符串最后的‘\0’。而判断一个字符串是否结束的标志就是看
    是否遇到‘\0’。如果遇到‘\0’,则认为本字符串结束。这个问题的关键就是要明白 char类型默认情况下是有符号的,其表示的值的范围为[-128,127],超出这个范围的值会产生溢出

分析以下代码段,解释输出的结果。

int main(int argc, char *argv[])
{
	char n[] = { 1, 0, 0, 0 };
	printf("%d\n", *(int *)n);
}
  1. 数组在内存中是从低到高按顺序存放的
  2. 小端模式下的读出是从高地址(高字节)读到低地址(低字节)
  3. 强制转换为int*类型,按大小端模式读出,如果是小端就是0000 0000 0000 0000 0000 0000 0000 0001(1的二进制存放)
    解释程序运行结果。
struct node 
{
 	char a;
 	short b;
 	int c;
};
int main(int argc, char *argv[])
{
 	struct node s;
 	memset(&s, 0, sizeof(s));
 	s.a = 3;
 	s.b = 5;
 	s.c = 7;
 	struct node *pt = &s;
 	printf("%d\n", *(int*)pt);
 	printf("%lld\n", *(long long *)pt);
}
  1. 结构体s的内存大小是8
  2. 强转后pt为int*类型,所占内存为0x0000 0x0001 0x0002 0x0003,读出为0x00050003,%d打印十进制为327683
  3. 强转后为long long long类型,所占内存为0x0000 0x0001 0x0002 0x0003 0x0004 0x0005 0x0006 0x0007,读出为0x0000000700050003,%lld打印十进制为30065098755
0x0000 0x0001 0x0002 0x0003 0x0004 0x0005 0x0006 0x0007
  03     00      05     00    07     00     00      00

在以下代码段执行完毕后,文件 Linux.txt 中的内容是什么?

int main(int argc, char *argv[])
{
	FILE *fp = fopen("Linux.txt", "wb");
	long long a = 0x78756e694c;
	fwrite(&a, sizeof(a), 1, fp);
	fclose(fp)
}
  1. long long long占8个字节,读出为linux,000000为字符串结束符
0x0000 0x0001 0x0002 0x0003 0x0004 0x0005 0x0006 0x0007 
4c(L)   69(i)  6e(n)  75(u)  78(x)     00     00    00

请简单叙述两种字节序(大端、小端)的概念,你的机器是什么字节序?试着写一个 C 语言程序来验证,如果你没有思路,你可以尝试着使用联合体或者指针。

int check_sys()
{
 	int i = 1;
 	return (*(char *)&i);
}
int main()
{
 	int ret = check_sys();
 	if(ret == 1)
 	{
 	printf("小端\n");
 	}
 	else
 	{
 		printf("大端\n");
 	}
int check_sys()
{
	union
	{
		int i;
		char c;
	}un;
	un.i=1;
	return un.c;
}
int main()
{
 	int ret = check_sys();
 	if(ret == 1)
 	{
 		printf("小端\n");
 	}
 else
 	{
 		printf("大端\n");
 	}
}

10.结构体存储

  1. 结构体的对齐规则:
  • 第一个成员在与结构体变量偏移量为0的地址处。
  • 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
  • 对齐数 = 编译器默认的一个对齐数与该成员大小的较小值。Linux中的默认值为4
  • 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
  • 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
  1. 为什么存在内存对齐
  • 平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
  • 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问
  • 总体来说
    结构体的内存对齐是拿空间来换取时间的做法
  • 所以设计结构体的时候,我们既要满足对齐,又要节省空间,就要让占用空间小的成员尽量集中在一起

以下代码段输出的两个值相等吗?为什么?

struct icd {
	int a;
	char b;
	double c;
};
struct cdi {
	char a;
	double b;
	int c;
};
int main(int argc, char *argv[])
{
	printf("%zu %zu\n", sizeof(struct icd), sizeof(struct cdi));
	//16 24
}

判断以下代码是否存在问题,如果有请找出并做适当的修改。

typedef struct a {
	char *name;
	int num;
} A;
void func(A *a)
{
	a = (A *)malloc(sizeof(A));
	strcpy(a->name, "XiyouLinuxGroup");
	a->num = 2018;
}
int main(int argc, char *argv[])
{
	A *a;
	func(a);
	printf("%s %d\n", a->name, a->num);
}
  1. 这里的char a[]=“abc”,可以更改数组中的字符,但是char本身是不可改变的常量。实际上先是在文字常量区分配了一块内存放"abc",然后在栈上分配一地址给a并指向,改变常量"abc"自然会崩溃
  2. char *a= “abc”,那么a是一个指针,其初值指向一个字符串常量,之后它可以指向其他位置,但如果试图修改字符串的内容,结果将不确定。
  3. 参数传递改为地址传递
typedef struct a {
	char name[100];
	int num;
} A;
void func(A *a)
{
	a = (A *)malloc(sizeof(A));
	strcpy(a->name, "XiyouLinuxGroup");
	a->num = 2018;
}
int main(int argc, char *argv[])
{
	A *a;
	a=func(a);
	printf("%s %d\n", a->name, a->num);
}

11.指针函数与函数指针

  1. 指针函数
    指带指针的函数,即本质是一个函数,函数返回类型是某一类型的指针
	void *pfun2()
  1. 函数指针
    指向函数的指针变量,即本质是一个指针变量。指向函数的指针包含了函数的地址,可以通过它来调用函数。
	void (*pfun1)();

pfun1先和*结合,说明pfun1是指针,指针指向的是一个函数,指向的函数无参数,返回值类型为void。

请解释下面代码的输出结果。

size_t q(size_t b)
{
 	return b;
}
size_t (*p(char *str))(size_t a)
{
 	printf("%s\n", str);
 	return q;
}
int main(int argc, char *argv[])
{
 	char str[] = "XiyouLinuxGroup";
 	printf("%lu\n", p(str)(strlen(str)));
}
  1. size_t(*p(char *str))(size_t a)这是一个返回值为函数指针函数(p(char *str))为函数指针,其传入值为(char *str),返回值为(size_t *(size_t a))型的函数指针.,即第二个q是一个指向第一个size_t q函数的指针,返回strlen(str)的大小

12.printf函数

下面代码会输出什么?

int main(int argc, char *argv[]) 
{ 
	 int a = 10, b = 20, c = 30;
	printf("%d %d\n", b = b*c, c = c*2) ;
	printf("%d\n", printf("%d ", a+b+c));
}
  1. printf的传入参数可变,并从右向左执行(相当于栈结构,左往右入栈,右往左出栈,再运算)
  2. printf的返回值为输出字符所占列宽,如果异常则返回负数
    注意空格

13.

说明下面程序的运行结果。

int main(int argc, char *argv[])
{
 	int a, b = 2, c = 5;
 	for(a = 1; a < 4; a++)
 	{
 		switch(a) 
 	{
 		b = 99;
 		case 2:
 		printf("c is %d\n", c);
 		break;
 		default:
 		printf("a is %d\n", a);
 		case 1:
 		printf("b is %d\n", b);
 		break;
 	}
 	}
}
  1. 编译器在执行switch语句时有优化(笔者的想法.)
  • 在case前面的语句不执行
  • 如果发生在case语句里面有初始化变量,比如
case 1:int m=1  break;

因为case中的变量属于整个switch块结构,编译器认为如果不执行case 1,m就会不被初始化而使用,所以编译通不过。
.

14.

请修改下面的 swap 函数,使得既可以交换 int 类型的参数,也可以交换 double 类型的参数。

void swap(int *a, int *b)
{
	int temp = *a;
	*a = *b;
	*b = temp;
}
  1. 复制字节
void swap(void* x, void* y,int size)  
{  
    void* t=malloc(size); 
    memcpy(t, x, size); 
    memcpy(x, y, size);  
    memcpy(y, t, size);  
   free(t);  
}  
int main()
{
    int  x=10;
    //double x=10;
    int  y=5;
    //double y=5;
    swap(&x,&y,sizeof(x));
    printf("%d %d",x,y);
    return 0;
}

15.

执行下列程序段,并输入“Xiyou Linux”(不含引号),那么程序的输出结果是什么?请解释其原因。

int main(int argc, char *argv[])
{
 	char *p = (char *)malloc(sizeof(char) * 20),*q=p;
 	//*q = p;
 	scanf("%s %s", p, q);
 	printf("%s %s\n", p, q);
}
  1. p 与 q 都是一个指针,指向的元素类型是 char* 类型, p 指向在堆上动态开辟的20个 char 类型的空间,将 p 赋值给 q 。这样,p 和 q 都是指向那块动态开辟的空间,指向的为同一块内存空间。
  2. 在输入数据时,首先输入 Xiyou ,这时 p 所指向的空间存储的数据就为 Xiyou 。由于 q 与 p 指向同一块空间,q 此时指向的也是Xiyou 。 这时又给 q 赋值,将Linux输入给q ,q=p ,Linux这时就将Xiyou给覆盖掉了。所以这个程序的输出结果为Linux Linux。

16.stdout,stderr

对比下面程序在 Linux 和 Windows 上的输出结果,并思考原因。

int main(int argc, char *argv[])
{
 	while(1)
 	{
 		fprintf(stdout, "Group ");
 		fprintf(stderr, "XiyouLinux");
 		getchar();
 	}
}
  1. stdout – 标准输出设备
  2. stderr – 标准错误输出设备。
  3. stdout输出到磁盘文件,stderr在屏幕。
  4. stdout是行缓冲的,它的输出会放在一个buffer里面,只有到换行的时候,才会输出到屏幕。而stderr是无缓冲的,会直接输出。
    两者均是在Linux下,而在Windows下,两者都默认无缓冲
  5. 键盘缓冲区,就是说你在键盘敲入的字符会存在键盘缓冲区里,当缓存区刷新的时候程序才会开始对你输入的数据流进行使用。(键盘缓冲区刷新在缓存区满了,或者遇到’\n’,或者程序结束时)
    举例来说就是fprintf(stdout,"xxxx");会缓存,直到遇到新行才一起输出。 而fprintf(stderr,"xxxxx");不管有没有\n,都输出。
  6. 当输入123’\n’后键盘缓冲区将其记录下来,遇到’\n’开始刷新,(程序开始使用数据)
  • 循环到了fprintf(stdout, “Group “);stdout有缓存,所以字符Group存在了buffer里面;

  • 继续到下一句fprintf(stderr, “XiyouLinux”);stderr没有缓存,XiyouLinux输出到屏幕上;

  • getchar()读入(循环3次)

  • 最后‘\n’被getchar();读入

  • 循环到了fprintf(stdout, “Group “);stdout有缓存,所以字符Group存在了buffer里面;(上面讲了遇到新行才输出)

  • 继续到下一句fprintf(stderr, “XiyouLinux”);stderr没有缓存,XiyouLinux输出到屏幕上;

  • ‘\n’读入结束,到新行,buffer里面存的字符打印出来

  • 所以有4个XiyouLinux四个Group

17.main函数

谈谈你对 main 函数的理解,可以从参数、返回值等角度分析。
int main(int argc, char argv[]){/…*/}

  1. 程序由主函数开始运行,由主函数结束,主函数可调用任何函数,任何函数都不可以调用主函数。
  2. argc:参数的个数。argv[]:是一个字符指针数组,每一个元素指向一个字符串。argv[0]:指向程序的路径名,argv[1…]:指向第n个参数对应的字符串。

18.c程序过程

C 语言从源程序到可执行程序需要经过哪些步骤?说说这些步骤都具体干了哪些工作。

  1. 预处理
    在这一阶段,源码中的所有预处理语句得到处理,例如#include语句所包含的文件内容替换掉语句本身,所有已定义的宏被展开,根据#ifdef,#if等语句的条件是否成立取舍相应的部分,预处理之后源码中不再包含任何预处理语句,生成.i文件。
  2. 编译
    这一阶段,编译器对源码进行词法分析、语法分析、优化等操作,最后生成汇编代码.s文件。这是整个过程中最重要的一步,因此也常把整个过程称为编译。
  3. 汇编
    这一阶段使用汇编器对汇编代码进行处理,生成机器语言代码,保存在后缀为.o的目标文件中。
  4. 链接
    链接的主要内容是把各个模块之间相互引用的部分处理好,将各个目标文件链接生成.exe可执行文件

下面是一个 C 语言程序从源代码到形成可执行文件的过程,请解释图中的 ABCD 分别表示什么,在每个阶段分别完成了什么工作?
在这里插入图片描述
A:预处理 —— 宏定义/条件编译/文件包含
B:编译 —— 检查语法,生成汇编
C:汇编—— 汇编代码转换成机械码
D:链接 —— 链接到一起生成可执行文件
注:预处理过程还会删除注释以及多余的空白符

  1. 第一阶段,预处理
    在预处理阶段,输入的是C语言源文件,通常为.c,它们一般带有h之类的头文件。这个阶段主要处理源文件中的#ifdef、#include和#define预处理命令。该阶段会生成一个中间文件.i
    预处理的过程主要处理包括以下过程:
  • 将所有的#define删除,并且展开所有的宏定义
  • 处理所有的条件预编译指令,比如#if #ifdef #elif #else #endif等
  • 处理#include 预编译指令,将被包含的文件插入到该预编译指令的位置。
  • 删除所有注释 “//”和”/* */”.
  • 添加行号和文件标识,以便编译时产生调试用的行号及编译错误警告行号。
  • 保留所有的#pragma编译器指令,因为编译器需要使用它们

生成预处理后的文件hello.i

$ gcc -E hello.c -o hello.i
$ gcc hello.c -E
$ cpp hello.c > hello.i    

它通过对源文件hello.c使用E选项来生成中间文件hello.i
直接cat hello.i 你就可以看到预处理后的代码

  1. 第二阶段,编译
  • 编译过程就是把预处理完的文件进行一系列的词法分析,语法分析,语义分析及优化后生成相应的汇编代码。

在编译阶段,输入的是中间文件.i,编译后生成汇编语言文件.s。这个阶段对应的gcc命令如下所示:
生成汇编语言文件hello.s

  $ gcc-s hello.i -o hello.s
  $ gcc hello.i -S
  $ /usr/lib/gcc/i486-linux-gnu/4.4/cc1 hello.c
  1. 第三阶段,汇编
  • 汇编器是将汇编代码转变成机器可以执行的命令,每一个汇编语句几乎都对应一条机器指令。汇编相对于编译过程比较简单,根据汇编指令和机器指令的对照表一一翻译即可。生成hello.o文件(目标文件)

在汇编阶段,将输入的汇编文件.s转换成二进制机器代码.o,这个阶段对应的gcc命令如下所示:

$ gcc -c hello.s-o hello.o
$ gcc –c hello.c –o hello.o
$ gcc hello.s -c
$ as hello.s –o hello.co

由于hello.o的内容为机器码,不能以普通文本形式的查看(vi 打开看到的是乱码)

  1. 第四阶段,链接
  • 通过调用链接器ld来链接程序运行需要的一大堆目标文件,以及所依赖的其它库文件,最后生成可执行文件。
  • 在链接阶段,将输入的二进制机器代码文件.o(与其他机器代码文件和库文件)汇集成一个可执行的二进制代码文件。

生成可执行文件

  $ gcc hello.o -o hello 
  $ gcc hello.o

总结版:
对应以上四个阶段,直接一个命令

$ gcc hello.c -o hello

你全看完了吗?c是世界上最好的语言

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值