C语言再学习之基础深挖

内联函数 C 中关键字 inline

调用函数时,一般会由于建立调用、传递参数、跳转到函数代码并返回等花费掉一些时间,而且一些函数被频繁调用,不断地有函数入栈,即函数栈,会造成栈空间或栈内存的大量消耗。

为了解决这个问题,在C99中特别地引入了inline修饰符,即内联函数。

关键字 inline 告诉编译器,任何地方只要调用内联函数,就直接把该函数的机器码插入到调用它的地方,类似于带参宏。

1
2
3
4
5
6
7
8
9
inline int max (int a, int b)
{
if (a > b)
return a;
else
return b;
}

a = max (x, y); // 等价于 "a = (x > y ? x : y);"

结构体 struct —— 构造数据类型

结构体(struct)就是一种把一些数据项组合在一起的数据结构类型,定义一个个人信息的结构体如下:

1
2
3
4
5
6
7
8
9
10
struct Info{
char name[10];
double height;
int age;
};
//定义一个 Info 类型的结构体变量
struct Info Mahoo;
//还可以用 typedef
typedef struct Info sInfo;
sInfo Mahoo;

将上述代码融合一下,常用来定义结构体的代码如下:

1
2
3
4
5
6
typedef struct Info{
char name[10];
double height;
int age;
}sInfo;
sInfo Mahoo;

结构体的对齐问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct Info1{
char name[3];
double id;
int age;
};
struct Info2{
double id;
char name[3];
int age;
};

//定义一个 Info 类型的结构体变量
struct Info1 Mahoo1;
struct Info2 Mahoo2;

int main(){
printf("Mahoo1:%d,Mahoo2:%d",sizeof(Mahoo1),sizeof(Mahoo2));
return 0;
}

代码输出为:Mahoo1:24,Mahoo2:16,可见同样的结构体内容,所占的字节大小是不一样的,而且在64位的编译器中,char :1 个字节,int: 4 个字节,double: 8 个字节,按理来说都应该是 15 个字节才对,因为涉及到字节对齐的缘故,在特定类型变量存储的时候,各种类型数据是按照一定的规则在空间上排列,而不是顺序的一个接一个的存放。

首先要明确编译器的对齐原则和一些基本概念:

  • 对齐:假定从零地址开始,每成员的起始地址编号,必须是它本身字节数的整数倍。其次,结构体本身也要根据自身的有效对齐值圆整(就是结构体成员变量占用总长度需要是对结构体有效对齐值的整数倍);

  • 数据类型自身的对齐值:char 型为 1,short 为 2,int,float 为 4,double类型为 8,单位字节。在Linux系统下计算时超过4字节按4字节计算;

  • 结构体的自身的对齐值:取决于其成员中自身对齐值最大的那个值;

  • 指定对齐值:通过#pragma pack (value)时动态地确定指定对齐值 value;

  • 数据成员和结构体的有效对齐值:自身对齐值和指定对齐值中较小的那个值,且结构体数据起始地址%有效对齐值 = 0 ;

好的,现在就拿上面的两个结构体例子说,首先假定地址是从零开始的,在 Info1 中,第一个成员char name[3],其对齐值为 1,占三个字节;所以第二个成员double id是从 3 开始,由于对齐的原则,该成员的有效对齐值为 8(无指定对齐值默认为 8),即double id真正的地址应该满足 %8 = 0,也就是对齐到 8 这个地址;第三个成员变量int age起始地址为 16,有效对齐值为自身对齐值 4,所以存储在 16~19 地址。

到这里结构体 Info1 变量的sizeof()应该为地址 0~19 共 20 个字节,但还要考虑到结构体的有效对齐值(自身对齐值为 double id的自身对齐值 8 ,指定对齐值未定)为 8 ,保证圆整,所以向上取整到 24 % 8 = 0。

Info2 也是按一样规则的对齐, double id地址为 0~7,char name[3]为 8~11,int age为 12~16,所以 Info2 变量的sizeof()为 16。

#pragma pack(n)

可以通过#pragma pack(n)设定结构体以 n 字节方式对齐,不仅仅是结构体,还可以是联合体,还有C++中类成员变量的对齐。n 可以取(124816) 中任意一值。例如:

1
2
3
4
5
6
7
8
9
10
#pragma pack(push)  // 保存对齐状态
#pragma pack(4) // 设定为 4 字节对齐

struct Info1{
char name[3];
double id;
int age;
};

#pragma pack(pop)

这样一编译,结构体 Info1 的大小也是 16 字节了。

对齐问题的启发

由上面的例子可以看出,不同的成员变量声明顺序有时决定了结构体不同的大小,如果想要节约空间的话,则可以把结构体中的变量按照类型大小从小到大声明(很容易得出),尽量减少中间的填补空间,当然可以采取强制措施解决。

常量 const —— 一个不能被改变的普通变量

const 一般用于定义常量或常量指针,所谓常量也就是不可更改的意思,但部分情况可以通过指针修改;当 const 修饰指针时,有两种情况:

  • 修饰一个指向常量的指针,则指针是可变的,常量不可变,const 在 * 的左侧;
  • 修饰指针为常量,即常量指针,指向的对象是可变的,const 位于 * 的右侧;
1
2
3
4
5
6
7
8
9
10
// 修饰常量
int const num;
const int num;
// 修饰指向常量的指针
const int *p;
int const *p;
// 修饰指针常量
int * const p;
// 都为常量
const int * const p;

另一种情况是修饰函数的形参,说明形参在函数内部不会被改变。例如修饰传递的指针形参时,不会修改实参指针所指向的数据:

1
void foo(int * const p);

char ** 数组与指针的恩恩怨怨

在探讨char **这个东西前,我们首先要明白:

  • 字符串常量的本质是一个地址:

    1
    2
    char *s;
    s = "hello";
  • 数组名在表达式中表示指向首元素的指针常量

    1
    2
    char a[] = "hello";
    a = s; // error

char **为二级指针,此类型定义的变量是指向一级指针char *的指针,常用于在函数传值时使用,如传递一级指针:

1
2
3
4
5
6
7
void fun(void){
    char* s;
init(&buf);
}
void init(int **tmp_s){
*tmp_s = malloc(10);
}

需要注意的时一个野指针的问题:

1
2
3
4
char** s;
printf("%d",s); // 0
*s = "hello"; // Variable 's' is uninitialized when used here
printf("%s",*s); // error and crash

定义 s 变量并为初始化,变量中默认存储的是 NULL 空指针 0,不指向任何内容;

所以在printf("%s",*s);时,程序会崩溃,无法继续执行;

因此使用char** s;时,需要对此进行初始化操作:

1
s = (char**)malloc(sizeof(char**));