2019-05-20 c 基础回顾(2)

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 其函数原型为unsigned int strlen(char *s); 其中s为指定的字符串。

​ 注意: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. 空类: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指向一个存放地址的表,只需要一个指针,因为所有虚函数地址都包含在这个表中。

  2. 若类中包含成员,则类对象的大小只包括其中非静态成员经过对齐所占的空间,对齐方式和结构体相同。如:

    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:进程与线程的关系和区别

关系和区别

一个线程可以创建和撤销另一个线程;

同一个进程中的多个线程之间可以并发执行。

相对于进程而言,线程是一个更加接近于执行体的概念,它可以与同进程中的其他进程共享数据,但是拥有自己的栈空间,拥有独立的运行序列。

区别主要有以下几点:

  1. 调度:进程是拥有资源的基本单位,线程是调度和分派的基本单位。
  2. 共享地址空间:进程拥有各自独立的地址空间、资源,所以共享复杂,需要用IPC(Inter-Process Communication,进程间通信),但是同步简单。而线程共享所属进程的资源,因此共享简单,但是同步复杂,需要用加锁等措施。
  3. 占用内存和cpu:进程占用内存多,切换复杂,cpu利用率低;而线程占用内存少,切换简单,cpu利用率高。
  4. 互相影响:进程之间不会互相影响;而一个线程挂掉会导致整个进程挂掉.

关于僵尸进程

产生原因:当子进程比父进程先运行结束,而父进程没有回收子进程时,子进程将会成为一个僵尸进程。如果父进程先退出了,那么子进程将会被init接管,从而就不会成为僵尸进程了,成为了孤儿进程。

如何避免僵尸进程的产生?

解决方法有以下几种:

​ 父进程通过wait或waitpid等待子进程结束,但是这会导致父进程挂起。
​ 如果父进程很忙,那么可以用signal函数为SIGCHLD安装handler,这样在子进程结束后,父进程会收到该信号,就可以在handler中调用wait回收。
​ 若父进程不关心子进程何时结束,那么可以用 signal(SIGCHLD, SIG_IGN) 通知内核自己对于子进程的结束不感兴趣,这样子进程结束后内核会回收,并不会再给父进程发送信号。
​ 让僵尸进程变为“孤儿进程”(即杀死其父进程),过继给1号进程init,init始终会负责清理僵尸进程。

你可能感兴趣的:(2019-05-20 c 基础回顾(2))