c语言中常见的一些坑和一些细节(建议收藏)

x33g5p2x  于2021-11-22 转载在 其他  
字(10.0k)|赞(0)|评价(0)|浏览(375)

博主接下来将会整理一些语言中常见的问题和坑,再看博主解释的时候可以自己思考一下
1.变量的声明和定义有什么区别?

答:变量的定义为变量分配地址和存储空间,变量的声明不分配地址。

一个变量的可以在多个地方声明,在只能在一个地方定义。加上extern修饰的是变量的声明,说明将这个变量在文件后面定义或者在文件以外

说明:

很多时候一个变量,只是声明不分配内存空间,直到使用时才初始化,分配内存

for example:外部变量

#include<stdio.h>
int main()
{
	extern int a;//注意在这里是声明不是定义声明a已经是一个已经定义了的外部变量,声明外部变量时可以将变量的类型去掉
	extern a;
}
int a;//此处是定义,定义了a为整型的外部变量

指针常量和常量指针有什么区别

答:指针常量是定义了一个指针,这个指针的值只能够在定义是初始化,在其他地方不能够改变。而常量指针是定义了一个指针,这个指针指向了一个只读对象,不能通过常量指针来修改这个对象的值,但这个对象本身不一定是常量。

注意:

无论是常量指针还是指针常量,其最大的用途就是作为函数的参数,保证实参在调用的时候的不可改变特性

#include<stdio.h>
int main()
{
	int a = 1;
	int* const p1 = &a;//p1为指针常量
	const int* p2 = &a;//p2为常量指针

}

strlen和sizeof的区别

答:1.sizeof是一个操作符,而strlen是一个库函数

2.sizeof的参数可以是数据类型,也可以是变量,而strlen只能以‘\0‘结尾的字符串作为参数

3.编译器在编译时就已经计算出sizeof的结果了,而strlen必须在运行时才能计算出来

4.sizeof计算出来的是数据类型所占的内存大小,而strlen计算的是字符串实际的长度

5.数组做sizeof的参数不退化,而传递给指针strlen就退化成指针了

结构体可以直接赋值吗?

答:声明时可以直接初始化,同一结构体的不同对象之间也可以直接赋值,但结构体中含有指针成员时一定要特别小心!!!!!!!!有可能多个指针指向了同一块内存时某个指针释放了这一段内存,可能会导致其他指针的非法操作。因此在释放前一定要保证其他指针不在使用这一块空间

sprintf,strcpy,memcpy有什么区别?

1.操作对象不一样,strcpy操作的两个对象均为字符串,sprintf的操作源对象可以是多种数据类型,目的操作对象是字符串,memcpy的两个对象是两个任意可操作内存的地址,并不限制于任何数据类型

2.执行效率不一样memcpy效率最高,将一个数组置零的最快方法就是使用memcpy,strcpy次之,而sprintf效率最低

3.实现功能不同,strcpy 主要实现字符串变量间的拷贝,sprintf 主要实现其他数据类型格式到字 符串的转化,memcpy 主要是内存块间的拷贝。 「注意」:strcpy、sprintf 与memcpy 都可以实现拷贝的功能,但是针对的对象不同,根据实际需求,来 选择合适的函数实现拷贝功能

#define和const用什么区别?

答:1.编译器处理方式不同:#define宏是在预处理阶段展开,不能对宏定义进行调试,而const常量是在编译阶段使用.

2.类型和安全检查不同:#define宏没有类型,不做任何类型检查,仅仅是代码展开,可能产生边际效应等错误,而const常量有具体类型,在编译阶段会执行类型检查和作用域检查;

  1. 存储方式不同:#define宏仅仅是代码展开,在多个地方进行字符串替换,不会分配内存,存储于程序的代码段中,而const常量会分配内存(可以是在堆中也可以是在栈中),但只维持一份拷贝,存储于程序的数据段中。 

定义域不同:#define宏不受定义域限制,而const常量只在定义域内有效

const定义常量从汇编的角度来看,只是给出了对应的内存地址,而不是象#define一样给出的是立即数,所以,const定义的常量在程序运行过程中只有一份拷贝(因为是全局的只读变量,存在静态区),而 #define定义的常量在内存中有若干个拷贝。
(6) 有些集成化的调试工具可以对const常量进行调试,但是不能对宏常量进行调试。
(7) 提高了效率。 编译器通常不为普通const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高。
(8) 宏替换只作替换,不做计算,不做表达式求解;宏预编译时就替换了,程序运行时,并不分配内存。

 

悬挂的指针和野指针有什么区别?

答:1.悬挂的指针:当指针所指向的对象已经被释放了但是该指针没有发生任何的改变,仍然指向已经被回收的那一块空间在这种情况下该指针被称为悬挂的指针

2.野指针:未初始化的指针被称为野指针

typedef和#define有什么区别?

1.用法不同:typedef 用来定义一种数据类型的别名,增强程序的可读性。define 主要用来定义 常量,以及书写复杂使用频繁的宏。

执行时间不同:typedef 是编译过程的一部分,有类型检查的功能。define 是宏定义,是预编译的部分,其发生在编译之前,只是简单的进行字符串的替换,不进行类型的检查。

作用域不同:typedef 有作用域限定。define 不受作用域约束,只要是在define 声明后的引用 都是正确的。 

对指针的操作不同:typedef 和define 定义的指针时有很大的区别。 

特别要注意:typedef 定义是语句,因为句尾要加上分号。而define 不是语句,千万不能在句尾加分号!!!!!!!!!!!!!!!!!!!!!!!!

 

如何避免野指针? 

1.指针变量声明时没有初始化 。方案:在指针声明时初始化,可以是具体的地址也可以用NULL来初始化。

2.指针变量被free或者delete后没有置成NULL。方案:指针指向的内存空间被释放后指针应该指向NULL。

  1. 指针操作超过了变量的作用域范围如返回局部变量的地址。方案:在变量作用域结束前释放变量的地址空间并让其指向NULL

栈和队列有什么区别

栈和队列都是存储结构。栈是先进后出,而队列是先进先出

补充:

栈区和堆区的区别:堆区的存储是“顺序随意的“而栈区是”先进后出“栈由编译器自动分配释放,存放函数的参数值局部变量的值等。类似于数据结构的栈。堆一般是由程序员手动释放

全局变量和局部变量有什么区别?操作系统和编译器是如何知道的?

1.全局变量是整个程序都可访问的变量,谁都可以访问,生存期在整个程序从运行到结束(在程序结束时所占内存释放)。

  1. 局部变量存在于模块(子程序,函数)中,只有所在模块可以访问,其他模块不可直接访问,模块结束(函数调用完毕),局部变量消失,所占据的内存释放。

3.操作系统和编译器,可能是通过内存分配的位置来知道的,全局变量分配在全局数据段并且在程序开始运行的时候被加载.局部变量则分配在堆栈里面
 

c语言中有那些不同的存储类说明符?

答: auto ,register,static,extern 

变量的范围是什么?变量在c语言中的作用域怎样

答:变量的范围是程序的一部分,可以直接访问该变量。在c语言中所有标识符都在静态范围内

如果没有分号怎样打印”hello world"

#include<stdio.h>

int main()

{

if(printf("hello world))

{

}

什么是NULL?

NULL是指针没用指向有效的位置,理想情况下,如果声明时不知道指针的值,应该将其初始化为NULL,当指针所指向的内存被释放的时候,我们应该将指针置为NULL

什么是内存泄漏?为什么要避免他? 

程序员在堆区开辟了一块内存而忘记释放它时就会发生内存泄漏。从定义可以看的出来内存泄漏是比较严重的问题,永远不会停止

什么是局部静态变量?他们有什么用?

局部静态变量是一个变量,其生命周期并不以声明他的函数调用而结束。它延长到整个程序的寿命。所有对该函数 调用都共享局部静态变量,静态变量可以用来计算调用函数的次数

局部变量未初始化默认值为0

什么是静态函数?他们有什么作用?

在c言中函数默认都是全局的,在函数前面加上static修饰使其变为静态的和全局函数不同的是对静态函数的访问仅限于声明他们的文件,因此当我们想要限制函数访问时我们可以将其设置为静态的

#include<>和#include""的区别是什么?

#include<>会先到系统指定目录寻找头文件

而#include""先到项目所在目录寻找头文件,如果没有找到再到系统指定目录寻找头文件

变量命名的规则是什么? 

变量名的开头只能是字母或者下划线 。变量名由数字,字母,下划线组成

数组的特点是什么?

同一个数组中所有的元素数据类型都是相同的?同时数组中所有成员在内存中的地址都是连续的 

数组的分类

数组的分类:数组可以分为静态数组和动态数组

静态数组:类似int arr[90];在程序运行时确定数组的大小,运行过程中不能改变数组的大小

动态数组:主要是在堆区申请的空间,数组的大小是在运行过程中确定的!!!可以更改数组的大小

一维数组不初始化,部分初始化,完全初始化的特点? 

1. 不初始化:如果是局部数组 数组元素的内容随机 如果是全局数组,数组的元素内容为0
2.部分初始化:未被初始化的部分⾃动补0
3.完全初始化:如果⼀个数组全部初始化 可以省略元素的个数 数组的⼤⼩由初始化的个数确定

指针和指针变量有什么区别  

1.指针:在内存中每一个字节都有一个地址 ,这个地址的编号就是指针。也就是说指针就是地址

2.指针变量:指针变量本质也是一个变量,用来保存地址。所以指针变量和指针是截然不同的概念

在使用realloc给已分配的堆区追加空间需要注意什么?

记得用指针变量保存realloc的返回值 

联合体和共用体的区别是什么?

在结构体中成员拥有独立的空间,

而共用体共享一块空间,每一个共用体的成员能访问共用区的空间大小是由成员自身的类型所决定 

如何理解浅拷贝和深拷贝 

1. 当结构体中有指针成员的时候容易出现浅拷⻉与深拷⻉的问题。
2.浅拷⻉就是,两个结构体变量的指针成员指向同⼀块堆区空间,在各个结构体变量释放的时候会出现多次释放同⼀段堆区空间
3.深拷⻉就是,让两个结构体变量的指针成员分别指向不同的堆区空间,只是空间内容拷⻉⼀份,这样在各个结构体变量释放的时候就不会出现多次释放同⼀段堆区空间的

普通局部变量,普通全局变量,静态局部变量,静态全局变量的区别

普通局部变量: 存在栈区、不初始化内容随机、只在定义所在的复合语句中有效、符合语句结束变量空间释放
普通全局变量 :存在全局区、不初始化内容为0、进程结束空间才被释放,能被当前源⽂件或其他源⽂件使⽤,只是其他源⽂件使⽤的时候,记得使⽤extern修饰
静态局部变量: 存在全局区、不初始化内容为0、整个进程结束空间才被释放,只能在定义所在的复合语句中有效
静态全局变量 :存在全局区、不初始化内容为0、整个进程结束空间才被释放,只能被当前源⽂件使⽤

描述一下gcc的编译过程

gcc编译过程分为4个阶段:预处理、编译、汇编、链接。
预处理:头⽂件包含、宏替换、条件编译、删除注释
编译:主要进⾏词法、语法、语义分析等,检查⽆误后将预处理好的⽂件编译成汇编⽂件。
汇编:将汇编⽂件转换成 ⼆进制⽬标⽂件
链接:将项⽬中的各个⼆进制⽂件+所需的库+启动代码链接成可执⾏⽂件
 

#和##的作用是什么? 

#是把宏参数转换为字符串的运算符,而##是把两个宏参数连接的运算符 

extern "C" 

extern "c“的作用是为了能够正确的实现c代码调用c语言代码。加上extern "C"后会指示编译器这一部分代码按c语言的编译方式进行编译,而不是c

(void )ptr和((void** ))ptr的结果是否相同?其中ptr为同一个指针

局部变量是否可以和全局变量重名? 

能,局部会屏蔽全局。要用全局变量,需要使用”::” ;局部变量可以与全局变量同名,在函数内引用这个变量时,会用到同名的局部变量,而不会用到全局变量。对于有些编译器而言,在同一个函数内可以定义多个同名的局部变量,比如在两个循环体内都定义一个同名的局部变量,而那个局部变量的作用域就在那个循环体内。
 

如何引用一个已定义过的全局变量? 

extern 可以用引用头文件的方式,也可以用extern关键字,如果用引用头文件方式来引用某个在头文件中声明的全局变理,假定你将那个编写错了,那么在编译期间会报错,如果你用extern方式引用时,假定你犯了同样的错误,那么在编译期间不会报错,而在连接期间报错。

全局变量可不可以定义在多个可被包含的.c文件包含头文件。为什吗? 

可以,在不同的C文件中以static形式来声明同名全局变量。 可以在不同的C文件中声明同名的全局变量,前提是其中只能有一个C文件中对此变量赋初值,此时连接不会出错.

语句for(;1;)有什么问题?他的意思是什么? 

和while(1)相同死循环 

do while();和while有什么区别 

一个先循环一遍在判断,一个判断以后赞循环  

代码篇

#include<stdio.h>
int main()

{
char a;
char *str=&a;
strcpy(str,"hello");
printf(str);
return 0;
}

 这段代码有什么问题?

没有为str分配内存空间,将会发生异常问题出在将一个字符串复制进一个字符变量指针所指地址。虽然可以正确输出结果,但因为越界进行内在读写而导致程序崩溃。

int(*s[10])(int)表示什么意思?

int(*s[10)(int)是一个函数指针数组,每一个元素指向了一个int func(int param)的函数 

以下代码有什么问题?  

#include<stdio.h>
int main()
{
char*s="AAA";
printf("%s",s);
s[0]='1';
printf("%s",s);

"AAA"是常量。s是指针变量,指向了这个字符串。在声明时就有问题。应该写成const char *s="AAA";又因为是常量,所以不能修改,所以s[0]=’1‘是非法的

下面这段代码会输出什么? 

void func(void)
{
unsigned int a=6;
int b=-20;
(a+b>6)?puts(">6"):puts("<=6");
}

6

当有符号和无符号运算时,同一转成无符号,而在有符号最高位的1表示负数,当转成无符号数时最高位不再表示负数所以是一个很大的数? 

下面这代码的结果是什么?

include <stdio.h> 
int main(void) 
{ 	
int i = -1; 	
if(i = 0) 		
printf("i = %d\n",i); 	

else if(i = 1) 		
printf("i = %d\n",i); 	
else 		
printf("i = %d\n",i); 	
return 0; 
}

1,=号是赋值而不是判断相等

这段代码有什么问题?

#include<stdio.h>
#include<string.h>
int main()
{
	char s[] = "abcdefg";
	char d[] = "123";
	strcpy(d, s);
	printf("%s %s", s, d);

}

结果:

咦?很奇怪,输出d是正确的,s发生了截断!要说越界也应该是d错啊,这是什么情况?大家不要着急,我们一步步来分析调试找原因。

      首先我们先来回顾一下strcpy函数的原理,把一个字符串复制到一个字符串上并在末尾追加空字符,但没有越界检验,安全性堪忧。但此题看运行结果是复制成功了,不应该是越界吗?

      那再往下就不能靠分析了,得调试程序找错了,说白了现在问题就在越界这里。为什么了这是因为局部变量是放在栈区的,栈区是先使用高地址在使用低地址。由于在这个编译器里面,变量之间的存放间隔很小,当strcpy越界的时候,就将s里面的类容给覆盖了
 

#include<stdio.h>
int main()
{
char *ptr;
if(ptr=(char*)malloc(0)==NULL)
printf("Got a null pointer);
else
printf("Got a valid pointer);
return 0;
}

以上代码会输出什么?

Got  a vail ponter

1.找错

void test ()
{
char string [10];
char *str1="0123456789";
strcpy(string ,str1);
}

这里string数组越界,因为字符串长度为10,还有一个结束符‘\0’。所以总共有11个字符长度。string数组大小为10,这里越界了。

注意:使用strcpy函数的时候一定要注意前面目的数组的大小一定要大于后面字符串的大小,否则便是访问越界。

 

void test2()
{
char string [10],str1[10];
for(i=0;i<10;i++)
{
str1[i]='a';
}
strcpy(string,str1);
}

这里有一个一眼就能看出的问题,那就是变量i没有定义,这在代码编译阶段编译器可以帮你发现,很容易搞定。然而很多问题是自己造成的漏洞,编译器是帮不上什么忙的。这里最大的问题还是str1没有结束符,因为strcpy的第二个参数应该是一个字符串常量。该函数就是利用判断第二个参数的结束符来得到是否拷贝完毕。所以在for循环后面应加上str1p[9] = ‘\0’;

注意:字符数组和字符串的最明显的区别就是字符串会被默认的加上结束符‘\0’。

 

void test3(char *str)
{
char string[10];
if(strlen(string )<=10)
{
strcpy(string ,str);
}
}

这里的问题仍是越界问题。strlen函数得到字符串除结束符外的长度。如果这里是<=10话,就很明显越界了。

下面这段代码的运行结果是什么?

#include<stdio.h>
int sum(int a)
{
auto int c=0;
static int b=3;
c+=1;
b+=2;
return a+b+c;
}
int main()
{
int i=0;
int a=2;
for(i=0;i<5;i++)
{
printf("%d ”,sum(a));
}
return 0;
}

运行结果是:8,10,12,14,16,

在求和函数sum里面c是auto变量,根据auto变量特性知每次调用sum函数时变量c都会自动赋值为0.b是static变量,根据static变量特性知每次调用sum函数时变量b都会使用上次调用sum函数时b保存的值。

简单的分析一下函数,可以知道,若传入的参数不变,则每次调用sum函数返回的结果,都比上次多2.所以答案是:8,10,12,14,16,

 

int func(int a)
{
int b;
switch(a)
{
case 1:b=30;
case 2:b=20;
case 3:b=16;
default :b=0;
}
return b;
}

func(1)的值为多少?

因为case语句中没有break,所以无论如何传给函数的参数是多少结果都是0

a[q-p]=? 

int a[3]; a[0]=0; a[1]=1; a[2]=2; int *p, *q; p=a; q=&a[2]; 很明显:a[q - p] = a[2] = 2;

下面我们来看一段很有趣的代码

#include <stdio.h>
 int main() 
{ int arr[] = { 6, 7, 8, 9, 10 };
 int *ptr = arr;
 *(ptr++) += 123;
 printf("%d,%d\n", *ptr, *(ptr++));

 return 0;
 }

结果为 8,7

程序运行结果为:
8,7
1.解释:*(ptr++) += 123这句代码中,使用了后++

所以效果相当于:ptr(6)+123=129;
ptr+=1;
也就是运行后
ptr指向larr[1];
2.解释:printf(“%d,%d\n”, ptr, (ptr++));这句代码的执行涉及到汇编的函数的参数的压栈过程是从后面的参数开始压栈的,所以*(ptr++)会先被压栈,导致ptr++首先生效(引用别人的 因为我不会汇编)

所以往栈里压入*ptr时,此时的ptr已经变成指向arr[2]了
同理一下代码的运行结果为8,8;
 

#include <stdio.h>
 int main() 
{ int arr[] = { 6, 7, 8, 9, 10 };
 int *ptr = arr;
 *(ptr++) += 123;
 printf("%d,%d\n", *ptr, *(++ptr));

 return 0;
 }

如果不是很理解的话,我们可以看一下这个例子

int i=2;
printf("%d\n",i++);
printf("%d\n",i);

原理是一样的

总结:

(p)++和p++的概括

(*p)++指的是先取出p指向的存储单元中的内容,然后将取出的数值加1,而p仍然指向原来的存储单元。

*p++则指的是先取出p指向的存储单元中的内容,然后将p值加1,此时p不再指向原来的存储单元。

表达式(p)++和p具有不同的含义,(p)++并没有修改指针p的指向,而p则修改了指针p的指向。
 

以下代码的结果是什么?

#include<stdio.h>
int i;
int main()
{
--i;
if(i>sizeof(i))
{
printf("大于");
}
else
{
printf("小于“);
}
return 0;
}

结果为:大于:解释首先全局变量未初始化默认值为0,--i之后变成-1,但是sizeof计计算出来的是无符号整型,在比较的时候会先将-1先转成无符号数那么-1在内存中的最高位不在是符号位,那么这将会是一个很大的数所以是大于

#include<stdio.h>
int main()
{
    union Data
    {
        struct 
        {
            int x;
            int y;
        }s;
        int x;
        int y;
    }d;
    d.x=1;
    d.y=2;
    d.s.x=d.x*d.x;
    d.s.y=d.y+d.y;
    printf("%d %d",d.s.x,d.s.y);
}

上面这段代码的运行结果是什么?

答:4,8.解释union中的成员相对于基地址的偏移量都为0,共用一块内存

d.x,d.y的起始位置都相同,共享内存空间。给任意一个赋值另外一个也会被赋予相同的值 

| | x |
| | y |
| s.y | s.x |

| | x,ys.x | s.y |
| d.x=1 | 1 | 0 |
| d.y=2 | 2 | 0 |
| d.s.x=d.x*d.x | 4 | 0 |
| d.s.y=d.y+d.y | 4 | 8 |
| | | |

最后分享一些比较坑的

| 优先级问题 | 表达式 | 误以为的结果 | 实际结果 |
| .的优先级高于*,->可以消除这个问题 | p.f | p所指向对象的字段f (p).f | p对f取偏移,作为指针然后进行解除引用操作(p.f) |
| []的优先级高于
| int *ap[] |
ap是一个指向int数组的指针

int(ap)[]
| ap是个元素为int指针数组int(ap[]) |
| 函数()高于
| int
fp() |
fp是一个指针,所指函数返回int

int(fp)()
|
fp是个函数返回int

int*(fp()) |
| ==和!=高于位操作符 | (val&mask!=0)  | (val&mask)!=0 | val&(mask!=0) |
| ==和!=高于= | c=getchar()!=EOF | (c=getchar())!=EOF | c=(getchar()!=EOF) |
| 算数运算高于移位运算 | msb<<4+lsb | (msb<<4)+lsb | msb<<(4+lsb) |
| 逗号运算符最低 | i=1,2 | i=(1,2) | (i=1),2 |

博主水平有限,如有错误请指正,本文来自网络收集和我自己的一些理解。如果觉得不错的可以点个赞,谢谢!

相关文章

微信公众号

最新文章

更多