一.简单的错误
请分析下⾯的程序,指出其中的问题
#include <string.h>
int main(void)
{
char *str = "linux";
str[0] = "L";
char a;
char *t = &a;
strcpy(t, str);
printf("%s\n", t);
}
运行该程序,很显然会报错,一是没有print的头文件#include<stdio.h>,二是因为str[0].
因为str是一个字符串指针,而字符串指针类似于const char str[],字符串指针指向的内容是不可修改的,用字符串指针定义的是存放在静态存储区,是常量,不可更改,想要将其更改正确,可以:
#include<stdio.h>
#include<string.h>
int main(){
char str[] = "linux";//想要更改可以用数组方式定义
char*p=str;//因为后面t为指针,所以复制也用指针。
str[0]='L';
char a;
char *t=&a;
strcpy(t,p);
printf("%s\n",t);
}
二.传递多维数组参数
请将三维数组 arr 作为实参传递给 func() 并写出 func() 的函数定义
int main(void)
{
int l = 5, m = 10, n = 20;
int arr[l][m][n];
}
这道题就是一个简单的数组传递,传递数组有值传递和地址传递,值传递效率比较低。
#include<stdio.h>
#include<stdlib.h>
// void func(int arr[][10][20]){//必须知道一维和二维的数值//方法一
// for(int i=0;i<5;i++){
// for(int j=0;j<10;j++){
// for(int k=0;k<20;k++){
// arr[i][j][k]=rand()%100+1;
// }
// }
// }
// }
void func(int (*arr)[10][20]){//方法二
for(int i=0;i<5;i++){
for(int j=0;j<10;j++){
for(int k=0;k<20;k++){
arr[i][j][k]=rand()%100+1;
}
}
}
}
// void func(int l,int m,int n,int arr[l][m][n]){//变长数组的传递方式
// for(int i=0;i<l;i++){
// for(int j=0;j<m;j++){
// for(int k=0;k<n;k++){
// arr[i][j][k]=rand()%100+1;
// }
// }
// }
// }
// void print(int arr[][10][20]){//输出方式一
// for(int i=0;i<5;i++){
// for(int j=0;j<10;j++){
// for(int k=0;k<20;k++){
// printf("%d ",arr[i][j][k]);
// }
// printf("\n");
// }
// printf("\n");
// }
// }
void print(int (*arr)[10][20]){//输出方式二
for(int i=0;i<5;i++){
for(int j=0;j<10;j++){
for(int k=0;k<20;k++){
//printf("%d ",arr[i][j][k]);
printf("%d ",*(*(*(arr+i)+j)+k));
}
printf("\n");
}
printf("\n");
}
}
int main(){
int l=5,m=10,n=20;
int arr[l][m][n];
//func(l,m,n,arr);//变长输出的调用
func(arr);
print(arr);
return 0;
}
三.纷乱的指针
int arr[10][5][3];
void *pr1 = (void *)arr;
int *pr2 = (int *)arr;
long *pr3 = arr;
char *pr4 = (char *)arr;
int *t[4];
t[0]=(int *)&arr[5];
t[1]=(int *)&arr[1][1][0];
t[2]=t[0];
t[3]=(int *)&t[0];
将 arr 的值记为: A
将 t 的值记为: B
将 char 类型的变量⼤⼩记为 1
int ⼤⼩为 4 , long ⼤⼩为 8
本题含⼀处语法错误,请找出。
请以 A+n 的形式说明以下内容的值:
&arr[0][0][0]
&arr[0][0][1]
&arr[0][1][0]
&arr[1][0][0]
&arr[1][0][1]
&arr[0][1][1]
arr
**arr+1
*(*arr+1)
**(arr+1)
*(*arr+1)+1
*(*(arr+1))+1
&arr[1][0][0]+1
&arr[0][2]+1
&arr[1]+1
&arr+1
pr1+2
pr2+2
pr3+2
pr4+2
请以 A+n 或 B+n 的形式说明下列值:
t
t+1
t[0]+1
&t[0]+1
*(&t[0]+1)+1
t[1]
(int(*)[4])t[1]
((int(*)[4])t[1])+1
*((int(*)[3][4])t[2]+1)+1
t[3]
t[3]-1
解答:
先对题目进行分析:
int arr[10][5][3];
void *pr1 = (void *)arr;//将三维数组变成一维,void*输出时需要强制类型转换为int*
int *pr2 = (int *)arr;//将三维数组变成一维
long *pr3 = arr;
//将int数组变化为long型数组,当用指针调用时,比如*(pr3+1)的+1地址为long的,是int的二倍,所以会跳过一个数。
//用pr3遍历会数组大小减半;
char *pr4 = (char *)arr;//强制类型转换为char*,输出是应强制转化回来
int *t[4];
t[0]=(int *)&arr[5];//三维数组中的第六个二维数组的第一个地址
t[1]=(int *)&arr[1][1][0];//三维数组中的第二个二维数组的第二个一维数组的第一个数的地址
t[2]=t[0];//和t[0]一样
// t[3]=(int *)&t[0];//有问题
int**p;//修改一下,也可以输出时强制类型转换:printf("%d\n",**(int**)t[3]);
p=t[3];
//或者
t[3]=(int*)&(*t[0]);
可以看出long *pr3=arr和t[3]有问题,并且long *pr3=arr没有强制类型转化,编译器可能会警告.
然后进行解答
// &arr[0][0][0]//A
// &arr[0][0][1]//A+1*4
// &arr[0][1][0]//A+3*4
// &arr[1][0][0]//A+15*4
// &arr[1][0][1]//A+16*4
// &arr[0][1][1]//A+4*4
// arr//A
// **arr+1//A+1*4
// *(*arr+1)//A+3*4
// **(arr+1)//A+15*4
// *(*arr+1)+1//A+4*4
// *(*(arr+1))+1//A+16*4
// &arr[1][0][0]+1//A+16*4
// &arr[0][2]+1//A+9*4
// &arr[1]+1//A+(15+15)*4=A+30*4
// &arr+1//A+150*4
// pr1+2//A+2*4
// pr2+2//A+2*4
// pr3+2//A+2*8
// pr4+2//A+2*1
// t//B,A
// t+1//A+72
// t[0]+1//A+4
// &t[0]+1//A+72
// *(&t[0]+1)+1//A+72+4
// t[1]//A+72
// (int(*)[4])t[1]//A+72
// ((int(*)[4])t[1])+1//A+72+16
// *((int(*)[3][4])t[2]+1)+1//A+64
// t[3]//B
// t[3]-1//B-4
四.查看二进制
输出⼀个 int 类型变量的⼆进制表⽰(要求:不进制转换)
#include<stdio.h>
void Binary(int num)//或者直接用循环/2的方法
{
int i;
for(i=31;i>=0;i--)//int类型下一共32位
{
int tnum=num;
tnum=num&(1<<i);
printf("%d",tnum>>i);
if(i%8==0){
printf(" ");
}
}
printf("\n");
}
int main(){
int num=0;
scanf("%d",&num);
Binary(num);
return 0;
}
五.宏函数
下⾯代码希望通过宏函数交换两个 int 类型的变量,请结合你对宏函数的理解,修改下⾯的代码,并谈 谈下⾯的代码的缺陷。
#define swap(x,y) int tmp=x;x=y;y=tmp;
首先,要知道宏是在预处理阶段的整体替换,不是函数,其次swap(x,y)习惯上会加“;”,而实际上,这个宏定义不需要‘;’,否则会出错,可以用do{}while(0)来避免。
#define swap(x,y) do{int temp=x;x=y;y=temp;}while(0)
注意,尽量避免在宏定义中++或--;
六.结构体内存对⻬(浅谈)
请谈谈你对 C 语⾔中, 结构体 中成员在内存中的存储⽅式的理解。
我们首先来看一个例子:
#include<stdio.h>
typedef struct {
int a;
char b;
double c;
char d[6];
}A;
typedef struct{
char b;
double c;
int a;
char d[6];
}B;
int main(){
A A1;
B B1;
printf("A=%d\nB=%d\n",sizeof(A1),sizeof(B1));
//A=24,B=32
return 0;
}
可以看出,A和B的数据类型是一样的,但占用的内存却不一样,但有一点是一样的,那就是结果都是8的倍数。
这就是内存对齐的结果,那么具体什么是内存对齐?为什么要内存对齐?
1.什么是内存对齐:
为了方便计算和说明,我们假设两个结构体都是从内存0开始的(实际并不可能),
对于结构体A:a占0~3的地址,b占4的地址,c是double类型,而4不是8的倍数,所以空下来4~7,从8开始,占据了8~15的地址,d是char类型长度为6的字符串,占据了16~21的地址,共24个地址。
对于结构体B:b占据了0~1的地址,c占据8~15的地址,a占据16~19的地址,d占据24~30的地址,然后发现最大数据类型为double,大小为8,所以补充(其实没有任何东西)至31,共32个地址。
这就是内存对齐,对齐规则是按照成员的声明顺序,依次分派内存,其偏移量为成员大小的整数倍,0看做任何成员的整数倍,最后结构体的大小为最大成员的整数倍。
可以注意到的一点就是我A中的d是从16开始的,可是16不是6的倍数啊,但16是8的倍数,是一个从寻址的长度(64位操作系统下),而内存对齐就是为了寻址方便,这就涉及到内存对齐的原理了。
2.内存对齐的原理(其实只是半个原理,更深的我也没完全搞懂):
计算机是以字节(Byte)为单位划分的,用cpu来访问某一字节的数据,但是cpu的寻址是通过地址总线来访问内存的,cpu又有32位和64位的区别,32位4字节,64位8字节,那么CPU实际寻址的长度就是4个字节,也就是只对地址是4的倍数的内存地址进行寻址,64位同理。
一个变量的数据存储范围是在一个寻址长度范围内的话,一次寻址就可以读取到变量的值,如果是超出了步长范围内的数据存储,就需要读取两次寻址再进行数据的拼接,效率明显降低了。
为了加快访问效率,减少不必要的消耗,就有了内存对齐。
参考: 浅谈CPU内存访问要求对齐的原因 – 仰望苍天思寰宇。
七.编译
请谈谈你对 C 语⾔使⽤中,「从源码到可执⾏⽂件」的过程的理解。(不是我说,这东西贼多,这里我就大概说说)
「从源码到可执⾏⽂件」的过程:源程序->编译预处理->编译->汇编程序->链接程序->可执行文件
1.预处理:处理以#开头的指令,比如头文件,宏定义;还有除去注释;条件预编译指令。
C语言的宏替换和文件包含的工作,不归入编译器的范围,而是交给独立的预处理器。
2.编译阶段 : 需要进行三个步骤:词法分析、语法分析和语义分析。
编译的过程实质上是把高级语言翻译成汇编语言的过程。
具体内容太多,就不过多解释了,这里放一个链接,有兴趣的可以去看:编译原理之词法分析、语法分析、语义分析_nic_r的专栏-CSDN博客_词法分析和语法分析的区别
3.汇编:将汇编代码转变成机器可以执行的指令(机器码文件)。
c语言编译,汇编会产生xxx.o的文件(unix和类unix),windows则是.obj.
4.链接:将翻译成的二进制与需要用到库绑定在一块。
链接可以执行与编译时(源代码被翻译成机器代码时),也可以执行与加载时(在程序被 加载器加载到存储器并执行时),甚至执行与运行时,由应用程序来执行.
链接由链接来完成,链接器还有静态链接器和动态链接器。
更深入的可以看看这个(偷个懒):c语言--从.c文件到可执行文件,其间经历了几步?O1,O2,O3是什么?_命z的博客-CSDN博客_.c文件到可执行文件经历哪些步骤
八. 括号匹配问题
编写⼀个函数,判断⼀个字符串中的括号是否匹配。
字符串中只有 ( 、 ) 、 [ 、 ] 、 { 、 } 这 6 字符, ( 必须与 ) 匹配, [ 必须与 ] 匹配, { 必须与 } 匹配。
例如:
{[()]()} 中的括号就是匹配的
([]{)}} 中的括号是不匹配的
请完善下⾯的函数:
// 函数接⼝定义
// 若匹配,返回1;若不匹配,返回0
int match(char *str);
先判断是否是偶数,不是偶数肯定不配对,然后我的思路是将左右括号分开。
#include<stdio.h>
#include<string.h>
char left(char s){
if(s==')'){
return '(';
}
if(s==']'){
return '[';
}
if(s=='}'){
return '{';
}
return 0;
}
int match(char*str){
int l=strlen(str);
if(l%2!=0){
return 0;
}
char arr[l+1];
memset(arr,0,l+1);
int k=0;
char ch;
for (int i = 0; i < l; i++)
{
ch=left(str[i]);
if(ch){
if(k==0||ch!=arr[k-1]){
return 0;
}
k--;
}else{
arr[k++]=str[i];
}
}
return k==0;
}
int main(){
char*str="{[()]}";
int bool=match(str);
printf("%d\n",bool);
return 0;
}
九. 判断链表是否有环
请以你能想到的最优的办法判断链表中是否有环。
// 链表结点结构体的定义
struct Node
{
int val;
struct Node *next;
};
#include<stdio.h>
typedef struct node
{
int val;
struct node *next;
}Node;
//使用双指针
//一个快,一次移动两个节点,一个慢一次移动一个节点
void isloop(Node*head){
Node *i,*j;
i=head;//慢
j=head;//快
while(j!=NULL&&j->next!=NULL){//注意:这里用快的判断
i=i->next;
j=j->next->next;
if(i==j){
printf("it is loop.");
}
}
printf("it not is loop.");
}
十. 递归反转链表
请使⽤ 递归 实现对链表的反转。
typedef struct node
{
int val;
struct node *next;
}Node;
Node* reverse(Node*head){
if( head==NULL || head->next==NULL ){
return head;
}
Node* NewHead=reverse(head->next);
head->next->next=head;
head->next=NULL;//防止成环
return NewHead;
}
//以下为测试用
void print(Node*head){
Node*d=head;
while(d!=0){
printf("%d ",d->val);
d=d->next;
}
printf("\n");
}
int main(){
Node*head=NULL;
Node*p;
Node*last=NULL;
int num=0;
do{
scanf("%d",&num);
if(num!=-1)
{
p=(Node*)malloc(sizeof(Node*));
p->val=num;
p->next=NULL;
if(head){
last->next=p;
}
else{
head=p;
}
last=p;
}
}while(num!=-1);
print(head);
head=reverse(head);
print(head);
return 0;
}