c 基础回顾
1. const #define 区别
推荐阅读
1)就起作用的阶段而言: #define是在编译的预处理阶段起作用,而const是在 编译、运行的时候起作用。
(2)就起作用的方式而言: #define只是简单的字符串替换,没有类型检查。而const有对应的数据类型,是要进行判断的,可以避免一些低级的错误。
(3)就存储方式而言:#define只是进行展开,有多少地方使用,就替换多少次,它定义的宏常量在内存中有若干个备份;const定义的只读变量在程序运行过程中只有一份备份。
(4)从代码调试的方便程度而言: const常量可以进行调试的,define是不能进行调试的,因为在预编译阶段就已经替换掉了。
2.经典C语言面试题8:sizeof与strlen的区别
1、sizeof是C/C++中的一个运算符,其作用是返回一个对象或者类型在内存中所占用的字节数。
注意:sizeof后面如果是类型则必须加括号,如 sizeof(char);而如果是变量名则可以不加括号,如 sizeof a; 但是建议使用时 均加上括号。sizeof不能返回动态地被分配的数组的大小。
2、strlen是C语言中的库函数,所在头文件为#include
注意:strlen只能用char *作为参数,它求的是字符串的实际长度,方法是从开始到遇到第一个'\0'结束。
sizeof是编译期就计算完成的,strlen是运行期计算的
所以 sizeof的具有常量性,sizeof的计算发生在编译时刻,所以它可以被当作常量表达式使用
当sizeof的对象是一个指针的时候呢?
学过数据结构的你应该知道指针是一个很重要的概念,它记录了另一个对象的地址。既
然是来存放地址的,那么它当然等于计算机内部地址总线的宽度。
所以在32位计算机中,一个指针变量的返回值必定是4(注意结果是以字节为单位)
在64位系统中指针变量的sizeof结果为8。
char* pc = "abc";
int* pi;
string* ps;
char** ppc = &pc;
void (*pf)();// 函数指针
sizeof( pc ); // 结果为4
sizeof( pi ); // 结果为4
sizeof( ps ); // 结果为4
sizeof( ppc ); // 结果为4
sizeof( pf );// 结果为4
数组的sizeof
数组的sizeof值等于数组所占用的内存字节数,如:
char a1[] = "abc";
int a2[3];
sizeof( a1 ); // 结果为4,字符 末尾还存在一个NULL终止符
sizeof( a2 ); // 结果为3*4=12(依赖于int)
一些朋友刚开始时把sizeof当作了求数组元素的个数,现在,你应该知道这是不对的,
那么应该怎么求数组元素的个数呢Easy,通常有下面两种写法:
int c1 = sizeof( a1 ) / sizeof( char ); // 总长度/单个元素的长度
int c2 = sizeof( a1 ) / sizeof( a1[0] ); // 总长度/第一个元素的长度
写到这里,提一问,下面的c3,c4值应该是多少呢
void foo3(char a3[3])
{
int c3 = sizeof( a3 ); // c3 ==
}
void foo4(char a4[])
{
int c4 = sizeof( a4 ); // c4 ==
}
也许当你试图回答c4的值时已经意识到c3答错了,是的,c3!=3。这里函数参数a3已不
再是数组类型,而是蜕变成指针,相当于char* a3,为什么仔细想想就不难明白,我
们调用函数foo1时,程序会在栈上分配一个大小为3的数组吗不会!
数组是“传址”的,调用者只需将实参的地址传递过去,所以a3自然为指针类型(char*)
c3的值也就为4。
当sizeof的对象是一个struct 类型变量的时候会出现什么问题呢?
struct : 变量对齐
经典C语言面试题4:字节对齐的作用
一、字节对齐的作用?
在现代计算机中,内存空间都是按照字节(byte)划分的。从理论上讲对任何类型的变量的访问可以从任何地址开始,但实际情况是,访问特定类型的变量的时候经常在特定的内存地址访问,这就需要各种类型的数据按照一定规则在空间上排列,而不是顺序地一个接一个地排放,这种所谓的规则就是字节对齐。这么长一段话的意思是说:字节对齐可以提升存取效率,也就是用空间换时间。
二、为什么需要字节对齐?
因为各个硬件平台对存储空间的处理上有很大的不同,一些平台对某些特定类型的数据只能从某些特定地址开始存取。比如有些架构的CPU在访问一个没有进行对齐的变量的时候会发生错误,那么在这种架构下编程必须保证字节对齐.其他平台可能没有这种情况,但是最常见的是如果不按照适合其平台要求对数据存放进行对齐,会在存取效率上带来损失。比如有些平台每次读都是从偶地址开始,如果一个int型(假设为32位系统)如果存放在偶地址开始的地方,那么一个读周期就可以读出这32bit,而如果存放在奇地址开始的地方,就需要2个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该32bit数据。显然在读取效率上下降很多。
三、如何对齐?
1.先确定实际对齐单位,其由以下三个因素决定
(1) CPU周期
WIN vs qt 默认8字节对齐
Linux 32位 默认4字节对齐,64位默认8字节对齐
(2) 结构体最大成员(基本数据类型变量)
(3) 预编译指令#pragma pack(n)手动设置 n--只能填1 2 4 8 16
上面三者取最小的,就是实际对齐单位(这里的“实际对齐单位”是我为了方便区分随便取的概念)
2.除结构体的第一个成员外,其他所有的成员的地址相对于结构体地址(即它首个成员的地址)的偏移量必须为实际对齐单位或自身大小的整数倍(取两者中小的那个)
3.结构体的整体大小必须为实际对齐单位的整数倍。
那如果结构体内部有个数组呢?那么就相对与有数组个数个数组类型的变量,
比如
typedef struct test{
double d;
char arr[12];
}test;
printf("%lu\n",sizeof(test));
==> 24
那么关于class类中的sizeof计算
-
空类:1
没有虚函数:sizeof(数据成员)的和
有虚函数:sizeof(数据成员)的和+sizeof(V表指针)=4例:
class no_virtual { public: void fun1() const{} int fun2() const { return a; } private: int a; } class one_virtual { public: virtual void fun1() const{} int fun2() const { return a; } private: int a; } class two_virtual { public: virtual void fun1() const{} virtual int fun2() const { return a; } private: int a; }
以上三个类中:
no_virtual没有虚函数,sizeof(no_virtual)=4,类no_virtual的长度就是其成员变量整型a的长度;
one_virtual有一个虚函数,sizeof(one_virtual)=8;
two_virtual有两个虚函数,sizeof(two_virtual)=8; 有一个虚函数和两个虚函数的类的长度没有区别,其实它们的长度就是no_virtual的长度加一个void指针的长度,它反映出,如果有一个或多个虚函数,编译器在这个结构中插入一个指针( V P T R)。在one_virtual 和two_virtual之间没有区别。这是因为V P T R指向一个存放地址的表,只需要一个指针,因为所有虚函数地址都包含在这个表中。 -
若类中包含成员,则类对象的大小只包括其中非静态成员经过对齐所占的空间,对齐方式和结构体相同。如:
class A { public: int b; float c; char d; }; sizeof(A)是12. class A { public: static int a; int b; float c; char d; }; sizeof(A)是12. class A { public: static int a; int b; float c; char d; int add(int x,int y) { return x+y; } }; sizeof(A)也是12.
3. 经典C语言面试题6:进程与线程的关系和区别
关系和区别
一个线程可以创建和撤销另一个线程;
同一个进程中的多个线程之间可以并发执行。
相对于进程而言,线程是一个更加接近于执行体的概念,它可以与同进程中的其他进程共享数据,但是拥有自己的栈空间,拥有独立的运行序列。
区别主要有以下几点:
- 调度:进程是拥有资源的基本单位,线程是调度和分派的基本单位。
- 共享地址空间:进程拥有各自独立的地址空间、资源,所以共享复杂,需要用IPC(Inter-Process Communication,进程间通信),但是同步简单。而线程共享所属进程的资源,因此共享简单,但是同步复杂,需要用加锁等措施。
- 占用内存和cpu:进程占用内存多,切换复杂,cpu利用率低;而线程占用内存少,切换简单,cpu利用率高。
- 互相影响:进程之间不会互相影响;而一个线程挂掉会导致整个进程挂掉.
关于僵尸进程
产生原因:当子进程比父进程先运行结束,而父进程没有回收子进程时,子进程将会成为一个僵尸进程。如果父进程先退出了,那么子进程将会被init接管,从而就不会成为僵尸进程了,成为了孤儿进程。
如何避免僵尸进程的产生?
解决方法有以下几种:
父进程通过wait或waitpid等待子进程结束,但是这会导致父进程挂起。
如果父进程很忙,那么可以用signal函数为SIGCHLD安装handler,这样在子进程结束后,父进程会收到该信号,就可以在handler中调用wait回收。
若父进程不关心子进程何时结束,那么可以用 signal(SIGCHLD, SIG_IGN) 通知内核自己对于子进程的结束不感兴趣,这样子进程结束后内核会回收,并不会再给父进程发送信号。
让僵尸进程变为“孤儿进程”(即杀死其父进程),过继给1号进程init,init始终会负责清理僵尸进程。