文章目录
- 写在前面
- Linux C开发
- 1. 定义一个宏,取两个值之间较小的值。
- 2. 描述一下define与typedef的区别
- 3. const与宏定义#define的区别
- 4. 如何区分指针常量与常量指针
- 5. 一个指针占几个字节?为什么?
- 6. sizeof()和strlen的区别和用法
- 7. 不使用C语言函数,实现strcpy与strlen相结合的函数
- 8. static是什么意思?如何使用?
- 9. static和extern有什么区别?
- 10. do{}while()和while()do{}的区别
- 11. 解释`void * (* (*fp1) (int)) [10];`
- 12. 什么是进程?什么是线程?
- 13. 进程与线程之间有什么联系和区别?
- 14.RTP传输为什么要用UDP而不是TCP?
- 15.sizeof(int**),其中int**占几个字节
写在前面
本文主要记录Linux C 开发、通信相关面试题,其中也有网络编程,Linux底层库相关知识。也会记录下在网上浏览到有意义的问题。
记录到此处,以便以后查看,也供大家参考,如果其中有错误答案或语句,还请指正。
本文将持续更新。
Linux C开发
1. 定义一个宏,取两个值之间较小的值。
#define MIN(x,y) ((x)<(y)?(x):(y))
注意点:
1、#define定义的宏名字一般为大写,为了与一般变量进行区分;
2、#define最后没有;
,如果加上分号,则引用宏时会将分号也引用进去;
3、 在宏定义中,表达式((x)<(y)?(x):(y))
最外面的的括号不能少,否则在宏展开后可能会产生歧义。
具体宏定义相关可参考另一篇文档:Linux C开发常见问题
2. 描述一下define与typedef的区别
定义不同
-
#define
为C语言中的预处理指令,不仅可以为类型取别名,还可以定义常量、变量、编译开关等。 -
typedef用来给已有的类型起一个别名,主要作用如下:
①. 简化复杂的类型声明,如
typedef bool (*FunPtr) (int, double); //声明了一个返回bool类型并带有两个形参的函数指针类型FunPtr
FunPtr func; //声明了一个FunPTR类型的函数指针对象func
②. 定义与平台无关的类型
③. 与struct
结合使用
区别
-
执行时间不一样
#define
执行时间在预处理阶段,也就是编译之前,它只是进行简单的字符串替换操作,而不进行任何检查。
typedef执行时间再编译阶段有效,因此有类型检查功能。 -
功能不同
typedef用来定义类型的别名,定义与平台无关的数据类型,常与struct结合使用;
#define
不仅可以为类型取别名,还可以定义常量、变量、编译开关等。
//typedef 原类型名 新类型名
typedef int INT;
//#define 新类型名 原类型名
#define INT int
-
作用域不同
#define
没有作用域限制,只要是之前定义过的宏,在以后的程序中都可以使用。
typedef 只能在定义的作用域中使用。 -
对指针的操作不同
见下面例子:
#define PINT int* //PINT为一个整形指针变量类型
typedef int* Pint;
int a = 3;
int b = 5;
const Pint p1 = &a; //相当于int* const p1,p1不可以更改,但是p1指向的内容可以更改
*p1 = 1; // 指向的内容被修改,正确。
p1 = &b; // p1的值被修改,错误。
const PINT p2 = &a; //相当于const int *p2,p2的值可以被修改,但是指针指向的内容不可更改
p2 = &b; // p2的值被修改,正确。
*p2 = 1; // p2指向的内容被修改,错误。
3. const与宏定义#define的区别
- 宏定义只是对值进行简单的替换,不进行类型检查;而const有具体的类型,在编译阶段会进行类型检查;
#define
宏是在预处理阶段展开,而const常量在编译运行阶段;- const定义的常量在程序的运行过程中只有一个拷贝,存储在静态区;而#define定义的常量在内存中有若干个拷贝;
- const效率高,因为编译器通常不为普通的const常量分配内存,而是保存在符号表中,没有读取和存储的操作,所以效率很高。
4. 如何区分指针常量与常量指针
一个简单但是不严谨的方法就是看*
和const
的位置,如果把*
看作指针,const
看作常量的话,如果*
在const
前面,则叫做指针常量;如果const
在*
前面,则叫做常量指针。如下:
int const *n; //常量指针
int * const n; //指针常量
更多关于指针常量和常量指针的解释可参考另一篇文章:Linux C开发常见问题->const关键字的用法
5. 一个指针占几个字节?为什么?
根据操作系统的位数不同,指针占用的字节数也不同。
- 64位的系统内存中,一个指针占用8个字节
- 32位的系统内存中,一个指针占用4个字节
- 16位的系统内存中,一个指针占用2个字节
为什么呢?经过在网上查找,可以这么简单的理解
拿32位的系统内存来说,也就是说有2^32个地址,通俗来说就是可以使用32个0或1的组合就可以找到内存中的任一地址,而32个0或1的组合也就是32位(32bit),换成字节就是4个字节。所以,32位的系统内的指针占用4个字节。同理,64位的系统内指针占用8个字节。
6. sizeof()和strlen的区别和用法
sizeof()
sizeof操作符以字节形式给出了其操作数的存储大小。操作数可以是一个表达式或在括号内的类型名。操作数的存储大小由操作数的类型决定。
上面这段话看着太难懂了,还是结合例子来说吧。
- 参数为数据类型或一般变量
例如sizeof(int),sizeof(long)等,这种情况要注意的是不同操作系统或者不同编译器得到的结果可能是不同的。例如int在16位操作系统中占2个字节,在32位系统中占4个字节,在64位系统中占8个字节。 - 参数为数组或指针
可以参考下面这个例子:
int a[50]; //sizeof(a) = 4*50 = 200;求数组所占空间的大小
int *a = new int[50]; //sizeof(a)= 4;因为a是一个指向int数组的指针。
总结:
sizeof 返回的值表示的含义如下(单位字节):
数组 —— 编译时分配的数组空间大小;
指针 —— 存储该指针所用的空间大小;
类型 —— 该类型所占的空间大小;
对象 —— 对象的实际占用空间大小;
函数 —— 函数的返回类型所占的空间大小。函数的返回类型不能是 void 。
sizeof(数组名)是求数组的整体大小
strlen
C库函数size_t strlen(const char *str)计算字符串str的长度,但不包括终止字符(\0).
可以参考下面这个例子:
#include <stdio.h>
#include <string.h>
int main() {
char str[50];
int len;
int size;
strcpy(str, "dasdasdsa");
len = strlen(str);
size = sizeof(str);
printf("length of str is %d, size of str is %d.\n", len, size);
return 0;
}
运行结果如下:
7. 不使用C语言函数,实现strcpy与strlen相结合的函数
原题目是让写出strcpy的实现,但是我又想了想,可以把这两个功能结合起来,尝试了一下,运行是没问题的 ?
#include <stdlib.h>
#include <stdio.h>
#include <assert.h>
int my_str(char *dest, char *src) {
int i = 0;
assert(dest != NULL);
assert(src != NULL);
while(src[i] != '\0') {
dest[i] = src[i];
i++;
}
dest[i] = '\0';
return i;
}
int main() {
int i = 0;
char dest[20];
char src[] = "dasdfafd";
i = my_str(dest, src);
printf("the dest str is %s, the lenth of dest str is %d.\n", dest, i);
return 0;
}
8. static是什么意思?如何使用?
static
可以修饰变量,也可以修饰函数。
修饰变量
局部变量
普通局部变量是再熟悉不过的变量了,在任何一个函数内部定义的变量(不加static
修饰)都属于这个范畴。编译器一般不会对普通局部变量进行初始化,也就是说它的值在初始时是不确定的,除非对其进行显示赋值。
普通局部变量存储与进城栈空间,使用完毕后会立即释放。
静态局部变量使用static
修饰符定义,即使在声明时未赋初值,编译器也会把它初始化为0。且静态局部变量存储于进程的全局数据区,即使函数返回,它的值也会保持不变。
变量在全局数据区分配内存空间;编译器自动对其进行初始化
其作用域为局部作用域,当定义它的函数结束时,其作用域随之结束
全局变量
全局变量定义在函数体外部,在全局数据区分配存储空间,且编译器会自动对其初始化。
普通全局变量对整个工程可见,其他文件可以使用extern
外部声明后直接使用。也就是说其他文件不能再定义一个与其相同名字的变量了(否则编译器会人为它们是同一个变量)。
静态全局变量仅对当前文件可见,其他文件不可访问,其他文件可以定义与其同名的变量,两者互不影响。
在定义不需要与其他文件共享的全局变量时,加上
static
关键字能够有效地降低程序模块之间的耦合,避免不同文件同名变量的冲突,且不会误使用。
修饰函数
函数的使用方法与全局变量类似,在函数的返回类型前加上static
,就是静态函数。其特性如下:
- 静态函数只能在声明它的文件中可见,其他文件不能引用该函数。
- 不同的文件可以使用相同名字的静态函数,互不影响。
非静态函数可以在另一个文件中直接引用,甚至不必使用
extern
声明。
拓展—面向对象
静态数据成员
在此类数据成员的声明前加上static
关键字,该数据成员就是类内的静态数据成员。其特点如下:
- 静态数据成员存储在全局数据区,静态数据成员在定义时分配内存空间,所以不能在类声明中定义
- 静态数据成员是类的成员,无论定义了多少个类的对象,静态数据成员的拷贝只有一个,且对该类的所有对象可见,也就是说任一对象都可以对静态数据成员进行操作。而对于非静态数据成员,每个对象都有自己的一份拷贝
- 由于上面原因,静态数据成员不属于任何对象,在没有类的实例时其作用域就可见,在没有任何对象时,就可以进行操作
- 和普通数据成员一样,静态数据成员也遵从public,protected,private访问规则
- 静态数据成员的初始化格式:<数据类型><类名>::<静态数据成员>=<值>
- 类的静态数据成员有两种访问方式:<类对象名>.<静态数据成员>或<类类型名>::<静态数据成员>
同全局变量相比,使用静态数据成员有两个优势:
- 静态数据成员没有进入程序的全局名字空间,因此不存在与程序中其他全局名字冲突的可能性
- 可以实现信息隐藏,静态数据成员可以是
private
成员,而全局变量不能
静态成员函数
与静态数据成员类似,静态成员函数属于整个类,而不是某一个对象,其特性如下:
- 静态成员函数没有
this
指针,它无法访问属于类对象的非静态数据成员,也无法访问非静态成员函数 - 出现在类体之外的函数定义不能指定关键字
static
- 非静态成员函数可以任意地访问静态成员函数和静态数据成员
9. static和extern有什么区别?
修饰全局变量
static
修饰全局变量,表示定义一个内部变量。extern
修饰全局变量,表示定义一个外部变量。
关于内部变量和外部变量
- 内部变量
- 定义的变量只能在该文件内部访问,别的文件访问不了该变量。
- 不同文件内可以存在同名的内部变量。
- 外部变量
- 定义的变量可以被同一工程内的其他函数访问。
- 同一工程中,同名的外部变量代表是同一变量,指向内存中的同一地址。
注意: 默认情况下,所有的全局变量均为外部变量
修饰函数
static对函数作用:表示定义和声明一个内部函数
extern对函数作用:表示定义和声明一个外部函数。
默认情况下函数均为extern
10. do{}while()和while()do{}的区别
-
do-while是先执行do里面的代码,再去判断while里面的条件是否满足,若满足,则重复执行do里面的代码;如不满足,则执行后面的代码。
-
while-do是先判断while里的条件是否满足,若满足,则执行do里面的代码;若不满足,则执行后面的代码。
可以参考下面这个例子:
#include <stdio.h>
int main()
{
int i = 11;
int j = 11;
while(i <= 10) {
i = i + 2;
}
do{
j = j + 2;
}while(j <= 10);
printf("i is %d, j is %d\n", i, j);
return 0;
}
运算结果如下:
[root@promote test]# ./do-while
i is 11, j is 13
11. 解释void * (* (*fp1) (int)) [10];
参考链接C语言复杂的声明和定义
首先,我个人来讲,看到这个问题,头皮发麻,不知道从何处下手。后来结合这个上面链接的讲解,一点一点,算是弄明白了,下面做一下总结,例子我就照着用了,不过中间会加一些自己的理解。
首先,我们来看一些简单的定义:
- 定义一个整形数
int a; - 定义一个指向整形数的指针
int *p; - 定义一个指向指针的指针,也就是双指针,它指向的指针指向一个整形数
int **pp;
然后下面就是说的它们三者的关系:
int a;
int *p;
int **pp;
p = &a; //p是一个指针,指向整形变量a
pp = &p; //pp是一个双指针,指向的是指针p
- 定义一个整形数组,其中包含10个整形数
int arr[10]; - 定义一个指向包含10个整形数数组的指针
int (*pArr)[10];
将4和5整合起来就是下面这样的:
int arr[10];
int (*pArr)[10];
pArr = &arr;
- 定义一个指向函数的指针,被指向的函数有一个整型参数并返回整型值
int (*pfunc)(int); - 定义一个包含10个指针的数组,其中包含的指针指向函数,这些函数有一个整型参数并返回整型值
int (*arr[10])(int);
将6和7结合起来是下面这样的:
int (*pfunc) (int);
int (*arr[10])(int);
arr[0] = pfunc;
在理解复杂定义的时候要遵循”右左法则“:
首先找到定义里面的变量,从它开始,先往右,再往左,碰到小括号就调转阅读的方向;小括号内分析完就跳出小括号,还是先向右,再向左。
套着上面的”右左法则“去理解一下6和7
int (*pfunc) (int)
首先,找到变量pfunc
,向右看,右边是小括号,那就调向左看,左边是一个*
,说明pfunc
是一个指针,当前小括号内都看完了,则跳出小括号,再向右看,又遇到小括号(int)
,说明*pfunc
是一个函数,所以pfunc
是一个函数指针,它指向的函数有一个int
类型的参数,右边看完了往左看,左边是一个int
,则说明pfunc
指向的函数返回值是int
类型。
再看7,int (*arr[10]) (int);
首先,同样的找到变量arr
,往右看是[]
,说明arr
是一个数组,再往左看是*
,说明数组arr
里面存的是指针(这里的*
修饰的不是arr
而是arr[10]
,因为[]
的运算优先级比*
高),当前小括号看完了,跳出小括号往右看是(int)
,那么说明arr
数组里存的指针指向的是函数,所指向的函数有一个int
类型参数,再往左看是int
,那么说明这个函数的返回值是int
类型,简而言之,就是说arr
是一个有10个元素的数组,数组里存的是指针,指针指向的是一个函数,这个函数有一个int
型的参数,并且返回值是int
型。
呼呼呼,感觉我的头发又掉了几根。。。
那么接下来分析题目void * (* (*fp1) (int)) [10];
首先找到变量fp1
,往右看是小括号,则调头向左看是*
,说明fp1
是指针,跳出小括号向右看是(int)
,说明fp1
是一个指向函数的指针,而且所指向的函数有一个参数是int
类型,再向右看是小括号,调头向左看是*
,说明该函数的返回值是指针,再跳一层小括号,向右看是[10]
,说明函数的返回值是一个数组指针,往右没了,再向左看是void *
,说明数组包含的类型是void *
。简而言之,fp1
是一个指向函数的指针,这个函数有一个int
类型参数,并且返回值是一个数组指针,数组里有10个元素,每个元素的类型是void *
。
里面的数组指针(定义5),函数指针(定义6),数组(定义7),很容易把人给弄混了,到底要怎么区分呢?可以简单的向下面这样去区分:
- 数组指针:
(*var) []
var
与*
是在同一个小括号里的,那么说明变量var
就是一个指针,[]
在后面,说明指针var
指向的是一个数组,那么就叫数组指针。 - 函数指针:
(*var) ()
var
与*
是在一起的,说明var
是一个指针,()
在后面,说明指针var
指向的是一个函数,这个函数的参数类型就是()
里面的类型(有可能有多个,比如(*var) (int, char *)
,指针var
指向函数的参数类型是int
和char *
),如果(*var)
左边也有类型,如int
,那么说明指针var
指向函数的返回值类型是int
。 - 数组:
(*var[])
var
和*
和[]
是同在一个小括号里的,但是这个时候var
会首先与[]
相结合,说明这是一个数组,而*
则表示数组里面存的是指针,有可能*
会变成int
,则说明数组里面存的是整型变量。
重复一遍,(*var)[]
里的变量var
是指针,(*var) ()
里的变量var
也是指针(*var[])
里的变量var
是一个数组
12. 什么是进程?什么是线程?
- 进程
进程是程序的一次执行过程,是程序在执行过程中分配和管理资源的基本单位,每一个进程都有自己的内存空间。
进程有五种状态:初始态,执行态,阻塞态,就绪状态,终止状态。 - 线程
线程是CPU调度和分派的基本单位,它可与同属一个进程的其他线程共享进程所拥有的全部资源。
13. 进程与线程之间有什么联系和区别?
- 联系
线程是进程的一部分,一个线程只能属于一个进程,而一个进程可以有多个线程,只有一个线程的进程称为单线程。 - 区别
-
根本区别:进程是操作系统进行资源分配的最小单位,而线程是任务调度和执行的基本单位。
-
开销方面:每个进程有自己独立的代码和数据空间,进程之间切换会有较大的开销;线程可以看做一个轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器,线程之间切换开销较小。
-
所处环境:在操作系统中可以运行多个进程;在一个进程中,可以运行多个线程。
-
内存分配:操作系统会为每个进程分配独立的内存空间;属于同一进程的线程共享该进程的内存空间。
-
包含关系:没有线程的进程可以看做是单线程的,如果一个进程有多个线程,则执行过程不是一条线,而是多条线并行进行,则称这个进程是多线程的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。
14.RTP传输为什么要用UDP而不是TCP?
首先,RTP也可以使用TCP来进行封装,只不过现在大多数是以UDP进行封装。
UCP和TCP均属于传输层的协议。
RTP一般用于实时性的音频或者视频传输,对实时性和传输速率要求较高。明白这个了我们来看一下TCP和UDP在这个方面的区别。
- TCP是面向连接的控制协议,也就是说,在传输TCP之前,需要双方先建立可靠的连接。之后客户端可以无差别的发送字节流到服务端。一般应用场景为双方对传输的可靠性要求较高时采用TCP。
- UDP是非面向连接的控制协议,相比于TCP而言,UDP无需等待双方建立可靠的连接,而是直接将数据包发送过去。一般应用在单次传输数据较少,并且对传输的可靠性要求不高的场景下。
从网上摘抄的关于TCP的主要功能:
TCP协议的主要功能是完成对数据报的确认、流量控制和网络阻塞;自动检测数据报,并提供错误重发的功能;将多条路径传送的数据报按照原来的顺序进行排列,并对重复数据报进行择取;控制超时重发,自动调整超时值;提供自动回复丢失数据的功能。
相对于TCP,UDP显然可以更好的契合音视频的实时性,原因如下:
- 最低开销
- 在最大数据从传输速率开始发送
- 不重复请求,所以就没有重传(实时性传输中,偶尔丢失一两个数据包是不影响通话的)
- 低处理时间,不需要缓冲
- 而且UDP的包头包含的字节远远低于TCP,利于解析
下面是从网上摘抄的关于TCP与UDP的区别,出处为什么RTP往往是使用UDP,而不是使用TCP封装
TCP | UDP | 和流媒体的关系 | |
---|---|---|---|
Header | 20Bytes | 8Bytes | UDP包头较小,节省系统解析包头的负载 |
Connection | Connection Oriented,在数据传输前需要建立connection | Connectionless,没有connection需要被建立 | 对于Multicast, ConnectionOriented是不合适 |
Reliability | 可靠ACK | 不可靠 | 可靠性比时间延迟(time delay)不重要,TCP会增加延时 |
Communication | Two-way 双向 | One way单向 | In UDP,RTCP implements the feedback:RTCP是相对于对RTP传输质量提供的反馈 |
Errors | Error Correction FEC在整个packet | Error Connection只在Header Checksum | UDP使用较少处理Errors时间 |
Data flow | 控制data flow用于管理下载速度 | 没有控制 | UDP sends to the same data flows as is encoded the media. |
Re-transmit | 需要Repeat | 不需要Repeat | Repeat也会产生延时,不适合实时应用 |
Delivery Rate | 没有预设。TCP将一直增加直到数据丢失或发现堵塞 | 传输速度和流的编码率相吻合 | UDP适应性更好 |
Client Buffer | Receive buffer overflow:如果数据到的太快,receiver发送一个信息给server,使其减慢传输 | 没有local caching,packet到了媒体播放器直接被处理 | Client Buffers也产生延时 |
15.sizeof(int**),其中int**占几个字节
首先明确int**是一个双指针,本地的操作系统是64位centos7.4,通过下面的代码打印可以看出int
占用4个字节(延续的32位系统),int*
占用8个字节,int**
占用8个字节。
#include <stdio.h>
int main()
{
printf("sizeof int is %d\n", (int)sizeof(int));
printf("sizeof int64 is %zu\n", sizeof(long long int));
printf("sizeof int* is %d\n", (int)sizeof(int*));
printf("sizeof int** is %d\n", (int)sizeof(int**));
return 0;
}
输出:
sizeof int is 4
sizeof int64 is 8
sizeof int* is 8
sizeof int** is 8
更多可参考Linux C开发常见问题