c语言面试知识总结

本文详细介绍了C语言面试中常见的知识点,包括const常量、static存储类别、void指针操作、内存分配方式、函数指针、宏定义与内联函数的区别、内存管理以及错误的代码示例和解析。特别强调了指针、内存管理、 volatile关键字、数组和结构体、运算符优先级等方面的问题,适合C语言开发者和面试者参考。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1、 Const:

(1)const修饰的是一个只读变量

(2)节省空间,避免不必要的内存分配,提高效率

编译器通常不为普通const只读变量分配存储空间,而是将它们保存在符号表中,这使

得它成为一个编译期间的值,没有了存储与读内存的操作,使得它的效率也很高。

例如:

#define M 3 //宏常量

const int N=5; //此时并未将N放入内存中

......

int i=N; //此时为N分配内存,以后不再分配!

int I=M; //预处理期间进行宏替换,分配内存

int j=N; //没有内存分配

int J=M; //再进行宏替换,又一次分配内存!

const定义的只读变量从汇编的角度来看,只是给出了对应的内存地址,而不是象#define

一样给出的是立即数,所以,const定义的只读变量在程序运行过程中只有一份拷贝(因为

它是全局的只读变量,存放在静态区),而#define定义的宏常量在内存中有若干个拷贝。

#define宏是在预处理阶段进行替换,而const修饰的只读变量是在编译的时候确定其值。

#define宏没有类型,而const修饰的只读变量具有特定的类型。

(3)修饰一般变量,数组,指针,函数参数,函数返回值

 

2、 Static

(1)    修饰全局变量,称为静态全局变量。由于全局变量本身存储在静态区,因此本身就是静态的,对全局变量使用静态是告诉编译器这个变量只能在本文件中被使用,不能被extern

(2)    修饰局部变量,称为静态局部变量。存储在静态区,即使函数下次调用也不改变其值。

(3)    修饰函数。表示这个函数的作用域仅限于本文件。

 

3、 如果一个函数没有显式地声明返回值,那返回值就是Int型的

在c语言中,如果一个函数没有显式地说明参数是void,那么是可以使用参数的,如下所示:

#include<stdio.h>

 

void test(){

         printf("ok\n");

}

 

int main(){

         //test(3);

         return0;

}

在c++中不可以


4、 按照ANSI(AmericanNational Standards Institute)标准,不能对void指针进行算法操作,

即下列操作都是不合法的:

void * pvoid;

pvoid++; //ANSI:错误

pvoid += 1; //ANSI:错误

ANSI标准之所以这样认定,是因为它坚持:进行算法操作的指针必须是确定知道其指

向数据类型大小的。也就是说必须知道内存目的地址的确切值。

例如:

int *pint;

pint++; //ANSI:正确

但是大名鼎鼎的GNU(GNU's Not Unix的递归缩写)则不这么认定,它指定void *的算法

操作与char *一致。因此下列语句在GNU编译器中皆正确:

pvoid++; //GNU:正确

pvoid += 1; //GNU:正确

在实际的程序设计中,为符合ANSI标准,并提高程序的可移植性,我们可以这样编写

实现同样功能的代码:

void * pvoid;

(char *)pvoid++; //ANSI:正确;GNU:正确

(char *)pvoid += 1; //ANSI:错误;GNU:正确

GNU和ANSI还有一些区别,总体而言,GNU较ANSI更“开放”,提供了对更多语法

的支持。但是我们在真实设计时,还是应该尽可能地符合ANSI标准。

【规则1-36】如果函数的参数可以是任意类型指针,那么应声明其参数为void *。

典型的如内存操作函数memcpy和memset的函数原型分别为:

void * memcpy(void *dest, const void *src,size_t len);

void * memset ( void * buffer, int c,size_t num );

这样,任何类型的指针都可以传入memcpy和memset中,这也真实地体现了内存操作

函数的意义,因为它操作的对象仅仅是一片内存,而不论这片内存是什么类型。如果memcpy

和memset的参数类型不是void *,而是char *,那才叫真的奇怪了!这样的memcpy和memset

明显不是一个“纯粹的,脱离低级趣味的”函数!

下面的代码执行正确:

例子:memset接受任意类型指针

int IntArray_a[100];

memset (IntArray_a, 0, 100*sizeof(int) );//将IntArray_a清0

例子:memcpy接受任意类型指针

int destIntArray_a[100],srcintarray_a[100];

//将srcintarray_a拷贝给destIntArray_a

memcpy (destIntArray_a, srcintarray_a,100*sizeof(int) );

有趣的是,memcpy和memset函数返回的也是void *类型,标准库函数的编写者都不是一

般人。

 

 

5、 void不能代表一个真实的变量。

因为定义变量时必须分配内存空间,定义void类型变量,编译器到底分配多大的内存呢。

下面代码都企图让void代表一个真实的变量,因此都是错误的代码:

void a; //错误

function(void a); //错误

void体现了一种抽象

 

 

6、一个定义为volatile变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。下面是volatile变量的几个例子:

1. 并行设备的硬件寄存器(如:状态寄存器)

2. 一个中断服务子程序中会访问到的非自动变量(Non-automaticvariables)

3. 多线程应用中被几个任务共享的变量

这是区分C程序员和嵌入式系统程序员的最基本的问题:嵌入式系统程序员经常同硬件、中断、RTOS等等打交道,所有这些都要求使用volatile变量。不懂得volatile内容将会带来灾难。

假设被面试者正确地回答了这是问题(嗯,怀疑是否会是这样),我将稍微深究一下,看一下这家伙是不是真正懂得volatile完全的重要性。

1. 一个参数既可以是const还可以是volatile吗?解释为什么。

2. 一个指针可以是volatile 吗?解释为什么。

3. 下面的函数被用来计算某个整数的平方,它能实现预期设计目标吗?如果不能,试回答存在什么问题:

1

2

3

4

intsquare(volatileint*ptr)

{

return*ptr**ptr;

}

下面是答案:

1. 是的。一个例子是只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。

2. 是的。尽管这并不很常见。一个例子是当一个中断服务子程序修改一个指向一个buffer指针时。

3. 这段代码是个恶作剧。这段代码的目的是用来返指针*ptr指向值的平方,但是,由于*ptr指向一个volatile型参数,编译器将产生类似下面的代码:

1

2

3

4

5

6

7

intsquare(volatileint*ptr)

{

inta,b;

a=*ptr;

b=*ptr;

returna*b;

}

由于*ptr的值可能在两次取值语句之间发生改变,因此ab可能是不同的。结果,这段代码可能返回的不是你所期望的平方值!正确的代码如下:

1

2

3

4

5

6

longsquare(volatileint*ptr)

{

inta;

a=*ptr;

returna*a;

}

 

 

7、 大小端问题值得注意:跟处理器有关,可以使用程序判定。

 

 

8、 enum

  在编译阶段确定其值

 

 

9、 const修饰的只读变量不能用来作为定义数组的维数,

也不能放在case关键字后面。

 

1、 Strlen和sizeof的区别

Strlen是一个函数,sizeof是一个运算符。

1、 sizeof(...)是运算符,在头文件中typedefunsigned int,其值在编译时即计算好了,参数可以是数组、指针、类型、对象、函数等。
    
它的功能是:获得保证能容纳实现所建立的最大对象的字节大小。
    
由于在编译时计算,因此sizeof不能用来返回动态分配的内存空间的大小。实际上,用sizeof来返回类型以及静态分配的对象、结构或数组所占的空间,返回值跟对象、结构、数组所存储的内容没有关系。
    
具体而言,当参数分别如下时,sizeof返回的值表示的含义如下:
    
数组——编译时分配的数组空间大小;
    
指针——存储该指针所用的空间大小(存储该指针的地址的长度,是长整型,应该为4);
    
类型——该类型所占的空间大小;
    
对象——对象的实际占用空间大小;
    
函数——函数的返回类型所占的空间大小。函数的返回类型不能是void

2、  strlen(...)是函数,要在运行时才能计算。参数必须是字符型指针(char*)。当数组名作为参数传入时,实际上数组就退化成指针了。
    
它的功能是:返回字符串的长度。该字符串可能是自己定义的,也可能是内存中随机的,该函数实际完成的功能是从代表该字符串的第一个地址开始遍历,直到遇到结束符NULL。返回的长度大小不包括NULL

3、 实际例子:

char arr[10] = "What?";
             int len_one = strlen(arr);

             int len_two = sizeof(arr); 
             cout << len_one << " and " <<len_two << endl; 

返回值为5 and 10,因为strlen计算的是字符串已经用掉的长度,因此应该为5,而sizeof返回的是获得保证能容纳实现所建立的最大对象的字节大小,这就表示的是这个数组的长度为10

char*t1[20];

   char (*t2)[20];

   printf("%d%d\n0",sizeof(t1),sizeof(t2));

返回值为80和4。*t1[20]是一个指针数组,本质上是一个数组,他表示一个长为20的数组,数组的每一位是一个指针,因此sizeof(t1)相当于在求一个数组的长度,而这个数组每一位所占的空间是一个指针的大小为4,因此总大小为80;(*t2)[20]是一个数组指针,本质上是一个指针,每个指针下面有20个空间大小,因此sizeof(t2)相当于t2[0]是一个指针,因此所占的大小为4。

 

2、 如何计算结构体的大小

首先需要明确,在c中,空结构体

structpoint{};

所占的大小为0,在c++中所占的大小为1。

structpoint{

   int num;

   char k;

   int c;

};

structpoint p;

   printf("%d\n",sizeof(p));

返回结果为12。这里我们先要明确一点:如何计算结构体的大小。运算符sizeof可以计算出给定类型的大小,对于32位系统来说,

sizeof(char) = 1; sizeof(int) = 4

基本数据类型的大小很好计算,我们来看一下如何计算构造数据类型的大小。

    C语言中的构造数据类型有三种:数组、结构体和共用体。

数组是相同类型的元素的集合,只要会计算单个元素的大小,整个数组所占空间等于基础元素大小乘上元素的个数。

结构体中的成员可以是不同的数据类型,成员按照定义时的顺序依次存储在连续的内存空间。和数组不一样的是,结构体的大小不是所有成员大小简单的相加,需要考虑到系统在存储结构体变量时的地址对齐问题。看下面这样的一个结构体:

       structstu1

       {

       int    i;

       char c;

       int j;

       }

先介绍一个相关的概念——偏移量。偏移量指的是结构体变量中成员的地址和结构体变量地址的差。结构体大小等于最后一个成员的偏移量加上最后一个成员的大小。显然,结构体变量中第一个成员的地址就是结构体变量的首地址。因此,第一个成员i的偏移量为0。第二个成员c的偏移量是第一个成员的偏移量加上第一个成员的大小(0+4,其值为4;第三个成员j的偏移量是第二个成员的偏移量加上第二个成员的大小(4+1,其值为5

实际上,由于存储变量时地址对齐的要求,编译器在编译程序时会遵循两条原则:

一、结构体变量中成员的偏移量必须是成员大小的整数倍(0被认为是任何数的整数倍)

二、结构体大小必须是所有成员大小的整数倍。

对照第一条,上面的例子中前两个成员的偏移量都满足要求,但第三个成员的偏移量为5,并不是自身(int)大小的整数倍。编译器在处理时会在第二个成员后面补上3个空字节,使得第三个成员的偏移量变成8

对照第二条,结构体大小等于最后一个成员的偏移量加上其大小,上面的例子中计算出来的大小为12,满足要求。

再看一个满足第一条,不满足第二条的情况

struct stu2

       {

       int   k;

       short t;

}

成员k的偏移量为0;成员t的偏移量为4,都不需要调整。但计算出来的大小为6,显然不是成员k大小的整数倍。因此,编译器会在成员t后面补上2个字节,使得结构体的大小变成8从而满足第二个要求。

3、 算术运算符 > 关系运算符 > 赋值运算符

因此:

X > y+2 也意味着x>(y+2)

X=y>2  也意味着 x=(y>2)

 

4、 在c语言中所有的输入实际上是一个输入流,可以用getchar来接收。

5、 内存分配方式:

内存分配方式有三种:
1从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static变量。
2在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。(在函数中不要返回栈内存,但可以返回动态分配的内存)。
3从堆上分配,亦称动态内存分配。程序在运行的时候用malloc new 申请任意多少的内存,程序员自己负责在何时用free delete释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多。

例子:
1

voidGetMemory(char*p)
{
     p = (char *)malloc(100);
}
void Test(void)
{
    char *str = NULL;
    GetMemory(str);
    strcpy(str, "hello world");
    printf(str);
}
请问运行Test 函数会有什么样的结果?
答:程序崩溃(段错误)。因为GetMemory并不能传递动态内存,Test函数中的str 一直都是 NULLstrcpy(str,"helloworld");将使程序崩溃。

2
char *GetMemory(void)
{
    char p = "hello world";
    return p;
}
void Test(void)
{
    char *str = NULL;
    str = GetMemory();
    printf(str);
}
请问运行Test 函数会有什么样的结果?
答:可能是乱码。
因为GetMemory返回的是指向栈内存的指针,该指针的地址不是 NULL,但其原现的内容已经被清除,新内容不可知。

3
void GetMemory2(char **p, int num)
{
    *p = (char *)malloc(num);
}
void Test(void)
{
    char *str = NULL;
    GetMemory(&str, 100);
    strcpy(str, "hello");
    printf(str);
}
请问运行Test 函数会有什么样的结果?
答:
1)能够输出hello
2)内存泄漏。

4
void Test(void)
{
    char *str = (char *) malloc(100);
    strcpy(str, “hello”);
    free(str);//
没有将str置为NULL
if(str != NULL)
{
    strcpy(str, “world”);
    printf(str);
}
}
请问运行Test 函数会有什么样的结果?
答:篡改动态内存区的内容,后果难以预料,非常危险。
因为free(str);

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值