相关文章链接 :
1.【嵌入式开发】C语言 指针数组 多维数组
2.【嵌入式开发】C语言 命令行参数 函数指针 gdb调试
3.【嵌入式开发】C语言 结构体相关 的 函数 指针 数组
4.【嵌入式开发】gcc 学习笔记(一) - 编译C程序 及 编译过程
5.【C语言】 C 语言 关键字分析 ( 属性关键字 | 常量关键字 | 结构体关键字 | 联合体关键字 | 枚举关键字 | 命名关键字 | 杂项关键字)
编译过程 :
编译过程 : 预处理 -> 编译 -> 汇编 -> 链接;
1. 编译预处理 : 产生 .i 后缀的预处理文件;
2. 编译操作 : 产生 .s 后缀的汇编文件;
3. 汇编操作 : 产生 .o 后缀的机器码二进制文件;
4. 链接操作 : 产生可执行文件 ;
预编译操作 :
预处理指令 : gcc -E test_1.c -o test_1.i
编译预处理示例 :
//预编译会将 stdio.h 中的内容拷贝到代码中,
#include
//注释会被替换成空格
//预编译中, HELLO_WOLD 会被原封不动的在代码中被替换为 "Hello World\n"
#define HELLO_WOLD "Hello World\n"
int main()
{
printf(HELLO_WOLD);
return 0;
}
test_1.i 出现了800多行的预处理文件, 原因是 #include < stdio.h >, 将 stdio.h 的文件拷贝了进来, 如果去掉了 #include 声明, 那么预处理文件就很小.
删除了 # include 代码 :
如果没有了 #include 声明, 那么预编译后的文件会大大减少.
编译 步骤中的操作 :
编译 需要的指令 : gcc -S test_1.c -o test_1.s ;
编译 示例 :
//预编译会将 stdio.h 中的内容拷贝到代码中,
//如果删除了 include 预编译, 那么代码量会大大减少
#include
//注释会被替换成空格
//预编译中, HELLO_WOLD 会被原封不动的在代码中被替换为 "Hello World\n"
#define HELLO_WOLD "Hello World\n"
int main()
{
printf(HELLO_WOLD);
return 0;
}
汇编 操作 :
汇编 命令 : gcc -c test_1.s -o test_1.o ;
每条汇编指令都对应着指定的机器码 .
汇编 过程示例 :
//预编译会将 stdio.h 中的内容拷贝到代码中,
//如果删除了 include 预编译, 那么代码量会大大减少
#include
//注释会被替换成空格
//预编译中, HELLO_WOLD 会被原封不动的在代码中被替换为 "Hello World\n"
#define HELLO_WOLD "Hello World\n"
int main()
{
printf(HELLO_WOLD);
return 0;
}
单步编译示例 :
//定义宏, 在预编译中会被删除, 直接替换到代码中
//预编译过程中 MIN(a,b) 会被 (((a)>(b)) ? (b) : (a)) 替换
#define MIN(a,b) (((a)>(b)) ? (b) : (a))
//定义全局变量
int global_variable = 666;
#include "test_1.h"
//定义两个宏
#define SMALL 666
#define BIG 888
int min(int a, int b)
{
//在预编译的步骤中, MIN(a, b) 直接替换为 (((a)>(b)) ? (b) : (a))
return MIN(a,b);
}
int main()
{
//预编译过程中, SMALL 被替换成 666, BIG 被替换成 888
int min_number = min(SMALL, BIG); // Call max to get the larger number
return 0;
}
分析 test_1.i 文件:
- 拷贝包含文件 : #include “test_1.h” 直接将 test_1.h 中的内容拷贝到 test_1.i 文件中 , 8 ~ 13 行是 test_1.h 文件拷贝到 test_1.i 中的内容.
- 编译器注释说明 : #部分不管, 是编译器生成的说明 ;
- 处理注释 : 将注释使用空格替换, test_1.i 中 8 ~ 12 行 5 行是空格, 第 8, 9, 12 行对应着 test_1.h 中的注释, 第十行对应着 test_1.h 中的宏定义, 第11行对应着空白行.
- 替换宏定义 : 将宏定义的位置替换到代码中, 宏定义行使用空格替代 , 其中 8 ~ 12 行空行, 第10行就是宏定义删除后的空行 ; 代码中 MIN(a,b) 的位置 被 (((a)>(b)) ? (b) : (a)) 替换, SMALL 被 666 替换, BIG 被 888 替换.
6.编译 产生 汇编文件 : 执行 gcc -S test_1.i -o test_1.s 命令 , 生成了 test_1.s 文件,
链接器简介 :
链接器 模块拼装 :
静态链接 :
静态链接图示 :
当运行2个a.out 时, 对于静态库 test_3.a 只需要加载 1 次, 但是对于 test_1.o 和 test_2.o 需要各自加载一次.
静态库链接内存图 :
动态链接 :
动态链接图解 :
宏定义 常量 :
//下面的宏定义都是合法的
//在预编译界面都是进行简单的代码文本替换
#define YES 1
#define PI 3.14
#define COUNTRY "China"
//出现 NAME 的位置使用 Bill 替换
#define NAME Bill
//这条宏定义是合法的, \ 是接续符号
#define PATH \root\apue\
io_code
宏表达式 #define :
#include
//宏定义表达式 加法表达式
#define SUM(a,b) (a)+(b)
//宏定义表达式 获取两个数之间较小的值
#define MIN(a,b) ((a
//宏定义表达式 获取数组中元素测试
#define DIM(array) (sizeof(array)/sizeof(*array))
//对比 #define SUM(a,b) (a)+(b) 宏定义, 方法不容易出现歧义
int sum(int a, int b)
{
return a + b;
}
//使用函数计算数组大小, 下面的语句是无法实现的
//array 传入之后, 在函数中会退化成一个指针, 其大小与元素大小一样
//sizeof(array) 是指针所占用的空间大小, 不是数组所占用的空间大小
int dim(int array[])
{
return sizeof(array)/sizeof(*array);
}
int main()
{
//获取 333 和 666 的和
printf("%d\n", SUM(333, 666));
//获取 333 666 之间较小的值
printf("%d\n", MIN(333, 666));
//这里我们想要得到 3 * 3 即 9, 但是编译执行后 结果是 5
//即使用 SUM(1,2) 替换为 (1)+(2)
//预编译后语句变为 : printf("%d\n", (1)+(2) * (1)+(2));
//注意点1 : 不要将宏表达式连续使用
printf("%d\n", SUM(1, 2) * SUM(1, 2));
//MIN(a++, b) 打印结果是 2
//如果出现了 a++ 等自增符号被宏替换
//预编译后替换结果 : printf("%d\n", ((a++
//注意点2 : 不要在宏替换中写 自增 自减 等其他表达式, 只使用简单的单一变量
int a = 1;
int b = 3;
printf("%d\n", MIN(a++, b));
//将 DIM(array) 宏替换, 计算数组大小, 打印结果为 7
//打印的语句被宏替换为 : printf("%ld\n", (sizeof(array)/sizeof(*array)));
//如果使用函数来计算数组大小,是无法实现的,如果函数传入 array, 函数参数 会将 array 当做一个指针,
//该array 数组就退化成了一个指针, 无法计算大小了, 该功能要比函数要强大
int array[] = {0, 1, 2, 3, 4, 5, 6};
printf("%ld\n", DIM(array));
//调用函数计算数组大小, 同样的语句打印出来的结果是1
printf("%d\n", dim(array));
return 0;
}
宏替换代码示例 :
#include
#include
//内存分配
#define MALLOC(type, x) (type*)malloc(sizeof(type)*x)
//死循环
#define FOREVER() while(1)
//用于替换 {} 的宏定义
#define BEGIN {
#define END }
//for 循环 宏
#define FOREACH(i, m) for(i = 0; i < m; i++)
int main()
{
int array[] = {1, 2, 3};
int i = 0;
//使用宏替换结果 : int *p = (int*)malloc(sizeof(int)*3);
int *p = MALLOC(int, 3);
//普通的for循环
//红替换结果 : for(i = 0; i < 3; i++)
FOREACH(i, 3)
BEGIN
p[i] = array[i];
printf("%d\n", p[i]);
END
//释放分配的 p 指针空间
free(p);
//在此处进行无限循环
//宏替换结果 : while(1);
FOREVER();
//这行end永远打印不出来了
printf("end\n");
return 0;
}
宏表达式 与 函数对比 :
递归代码示例 (错误示例) :
#include
//定义一个递归宏, 这种定义时错误的, 宏表达式中不能出现递归
#define FAC(n) ((n > 0) ? (FAC(n - 1) + n) : 0)
//递归函数
int fac(int n)
{
return ((n > 0) ? (fac(n - 1) + n) : 0);
}
int main()
{
//该步骤报错
printf("%d\n", FAC(10));
return 0;
}
宏定义作用域限制 :
#include
//宏定义 常量 和 宏定义表达式没有作用域限制
//宏定义可以出现在代码的任何位置, 定义完毕之后可以再任何位置调用
int min_1(int a, int b)
{
//任意位置定义的宏可以在任意地方使用, 没有作用域限制
#define MIN(a, b) ((a) < (b) ? a : b)
//直接宏替换为 : return ((a) < (b) ? a : b);
return MIN(a, b);
}
int min_2(int a, int b, int c)
{
//直接宏替换为 : return ((((a) < (b) ? a : b)) < (c) ? ((a) < (b) ? a : b) : c);
return MIN(MIN(a, b), c);
}
int main()
{
printf("%d\n", min_1(1, 2));
printf("%d\n", min_2(4, 2, 3));
return 0;
}
限制宏定义作用域 #undef 用法 :
#include
//宏定义 常量 和 宏定义表达式没有作用域限制
//宏定义可以出现在代码的任何位置, 定义完毕之后可以再任何位置调用
//#undef 可以限制 #define 作用域
int min_1(int a, int b)
{
//任意位置定义的宏可以在任意地方使用, 没有作用域限制
#define MIN(a, b) ((a) < (b) ? a : b)
//直接宏替换为 : return ((a) < (b) ? a : b);
return MIN(a, b);
//这里取消 MIN 宏定义, 限制其作用域只能在该范围之内使用, 之后就不可使用 MIN 了
#undef MIN
}
int min_2(int a, int b, int c)
{
//此处无法使用 MIN 宏, 上面使用了 #undef MIN 限制了宏定义的使用范围
return MIN(MIN(a, b), c);
}
int main()
{
printf("%d\n", min_1(1, 2));
printf("%d\n", min_2(4, 2, 3));
return 0;
}
内置宏举例 :
使用宏定义日志打印 :
#include
#include
//1.日志宏, 用函数反而达不到打印日志行号的效果, 因此这里使用宏最好
#define LOG(s) printf("%s : %d : %s\n", __FILE__, __LINE__, s);
//2.使用函数打印日志, 但是调用处的行号无法准确显示
//调用该函数打印出的日志, 其行号始终是函数中的行号
//使用函数打印日志, 无法获取行号, 因此我们使用宏来实现
void Log(char* s)
{
//__FILE__ 是内置宏, 代表 本文件文件名
//__LINE__ 是内置宏, 代表 当前行号
printf("%s : %d : %s\n", __FILE__, __LINE__, s);
}
//3.尝试打印时间的函数
void log_time()
{
time_t t;
struct tm* p;
//获取当前时间
time(&t);
//时间转换
p = localtime(&t);
printf("%s", asctime(p));
}
//4.定义打印时间 行数 文件 名的宏日志
//多行宏定义可以使用 do{}while(0) 来实现
#define LOG_TIME(s) do{ \
time_t t; \
struct tm* p; \
time(&t); \
p = localtime(&t); \
printf("time : %sfile : %s \nline : %d\ncontent : %s\n", asctime(p), __FILE__, __LINE__, s); \
}while(0)
int main()
{
Log("函数打印日志1");
Log("函数打印日志2");
LOG("宏打印日志1");
LOG("宏打印日志2");
log_time();
LOG_TIME("日志宏带时间");
return 0;
}
日志宏 : 打印日志的同时, 打印当前的文件名称, 代码行号, 当前运行时间 ;
条件编译指令 :
条件编译的应用环境 :
条件编译 注意点 :
通过修改代码 控制 条件编译 代码示例 :
#include
//预编译阶段,如果发现
//也可以在gcc -E 命令中指定 #define 常量
//gcc -DC=1 -E test_1.c -o test_1.i 可以进行同样的预编译处理, 即使没有定义这个宏
//gcc -DC=1 test_1.c 可以在预编译阶段生成下面同样宏定义
#define C 1
int main()
{
#if(C == 1)
printf("1\n");
#else
printf("2\n");
#endif
return 0;
}
修改代码后 删除宏定义 :
#include
int main()
{
#if(C == 1)
printf("1\n");
#else
printf("2\n");
#endif
return 0;
}
上述两个例子, 主要是通过在代码中定义 宏常量, 来控制条件编译中, 哪些语句需要编译, 哪些语句在预编译阶段就要删除 ;
使用命令行定义宏 从而控制条件编译, 代码不变 :
#include
int main()
{
#if(C == 1)
printf("1\n");
#else
printf("2\n");
#endif
return 0;
}
#include 间接包含 :
间接包含 结构图示 : test_1.c 文件包含 三个头文件, test_1.h 包含的 test_2.h 头文件 与 test_1.c 包含的该头文件相同, 同一个头文件被导入了2次, 因此编译时会报错;
间接包含 代码示例 :
#include
#include "test_1.h"
#include "test_2.h"
int main()
{
fun();
printf("%s\n", HELLO);
return 0;
}
#include
#include "test_2.h"
char* HELLO = "Hello World";
void fun()
{
printf("test_1.h Hello");
}
int test_2_variable = 666;
间接包含 简单解决方案 : 下面的代码与上面的唯一区别是, test_1.c 中注释掉了 #include “test_2.h” 语句.
#include
#include "test_1.h"
//#include "test_2.h"
int main()
{
fun();
printf("%s\n", HELLO);
return 0;
}
#include
#include "test_2.h"
char* HELLO = "Hello World";
void fun()
{
printf("test_1.h Hello");
}
int test_2_variable = 666;
使用 #ifndef , #define 和 #endif 语句处理头文件包含情况 :
#include
//使用了 #ifndef #endif 宏 控制编译头文件, 任意包含几次头文件都不会出错
#include "test_1.h"
#include "test_1.h"
#include "test_2.h"
#include "test_2.h"
int main()
{
fun();
printf("%s\n", HELLO);
return 0;
}
//如果没有定义 _TEST_2_H_ 宏, 才扩展下面的内容
//如果已经定义了 _TEST_2_H_ 宏, 那么从 #ifndef 到 #endif 之间的内容都要扩展进去
//一般情况下定义的宏名称是 头文件变成大写
#ifndef _TEST_1_H_
#define _TEST_1_H_
#include
#include "test_2.h"
char* HELLO = "Hello World";
void fun()
{
printf("test_1.h Hello");
}
#endif
//如果没有定义 _TEST_2_H_ 宏, 才扩展下面的内容
//如果已经定义了 _TEST_2_H_ 宏, 那么从 #ifndef 到 #endif 之间的内容都要扩展进去
//一般情况下定义的宏名称是 头文件变成大写
#ifndef _TEST_2_H_
#define _TEST_2_H_
int test_2_variable = 666;
#endif
条件编译控制代码示例 :
#include <stdio.h>
//控制开发版本与发布版本 :
//如果定义了 DEBUG 宏, 那么LOG(s) 就会打印调用位置的文件和行号以及对应日志
//如果没有定义 DEBUG 宏, 那么 LOG(s) 就会直接使用 NULL 替换
#ifdef DEBUG
#define LOG(s) printf("%s : %d : %s \n", __FILE__, __LINE__, s)
#else
#define LOG(s) NULL
#endif
//控制不同的产品编译
//如果定义了 PRODUCT_1, 那么编译上面的 fun(), 删除下面的 fun()
//如果没有定义 PRODUCT_1, 那么删除上面的 fun(), 编译下面的 fun()
#ifdef PRODUCT_1
void fun()
{
LOG("product 1 fun start");
printf("product 1 fun() \n");
LOG("product 1 fun end");
}
#else
void fun()
{
LOG("product 2 fun start");
printf("product 2 fun() \n");
LOG("product 2 fun end");
}
#endif
int main()
{
//控制日志打印
LOG("main() start");
//根据当前定义的产品打印不同的结果
#ifdef PRODUCT_1
printf("product 1 welcom\n");
#else
printf("product 2 welcom\n");
#endif
fun();
LOG("main() end");
return 0;
}
#error简介 :
#warning 也是编译指示字, 用于在编译时生成警告信息, 但是编译的过程不会终止, 会继续编译下去 ;
#error #warning 代码示例 :
#include
int main()
{
//如果没有定义 MAX 宏, 预编译过程中就会终止编译过程, 不会生成 test_1.i 文件;
#ifndef MAX
#wraning 没有定义MAX宏,即将退出!
#error 没有定义MAX宏,已退出!
#endif
printf("程序执行完毕!\n");
return 0;
}
#line 简介 :
#line 使用代码示例 :
#include
int main()
{
//使用 #line 设置 行号 和 文件名
#line 100 "test_1_han.c"
printf("行号 : %d , 文件名 : %s \n", __LINE__ , __FILE__);
return 0;
}
#pragma 编译器指示字 简介 :
#pragma 用法 : #pragma 参数
#pragma message 参数 :
#include
//如果宏定义中定义了 MAX 宏, 那么输出信息 编译 MAX
#if defined(MAX)
#pragma message("编译 MAX")
#define VERSION "MAX"
//如果宏定义中定义了 MIN 宏, 那么输出信息 编译 MIN
#elif defined(MIN)
#pragma message("编译 MIN")
#define VERSION "MIN"
//如果既没有定义 MAX 也没有定义 MIN, 那么直接报错停止编译
#else
#error 需要定义VERSION宏!
#endif
int main()
{
printf("%s\n", VERSION);
return 0;
}
内存对齐 简介 :
结构体 struct 占用内存计算方式 :
#include
//结构体中元素计算总共有 3 字节, 但是其事实上占 4 字节
//但是 CPU 读取内存时一次性读取 2 的n次方个字节 1, 2, 4, 8, 16 字节
//①性能考虑 : 如果3字节的话 需要先读取1字节, 再读取2字节,
//这样就得读取两次, 因此比较消耗性能, 索性将其分配4字节,CPU 可以一次读取
//②硬件平台限制 : 硬件平台可能只支持读取偶地址, 如果读取到了奇数地址, 直接报硬件异常
struct struct_1
{
//占 1 字节
char c;
//占 2 字节
short s;
};
/*
内存对齐分析 :
1. char c, 对齐参数是 char 大小1 和 对齐数 2 中的较小值 为 1, 第一个起始位置没有要求 , 起始位置是0, 大小占 1字节, 之后的起始位置 1;
2. short s, 对齐参数是 short 大小2 和 对齐数 2 中的较小值 为 2, 起始位置要整除 对齐参数2,起始位置 2, 占 2 字节, 之后的起始位置 4;
3. char c2, 对齐参数是 char 大小1 和 对齐数 2 中的较小值 为 1, 起始位置要整除 对齐参数1,起始位置 4, 占 1 字节, 之后的起始位置 5;
4. int i, 对齐参数是 int 大小4 和 对齐数 2 中的较小值 为 2, 起始位置要整除 对齐参数2,起始位置 6, 占 4 字节, 之后的起始位置 10;
计算大小 为 6 + 4 = 10;
最后要求 : 最终的大小必须是整除所有的对齐参数, 即 1 和 2, 大小 10满足要求 ;
最终的计算大小为 10;
*/
#pragma pack(2)
struct struct_2
{
char c;
short s;
char c2;
int i;
};
#pragma pack()
/*
内存对齐分析 :
1. char c, 对齐参数是 char 大小1 和 对齐数 4 中的较小值 为 1, 第一个起始位置没有要求 , 起始位置 0, 占 1 字节, 之后的起始位置 1;
2. char c2, 对齐参数是 char 大小1 和 对齐数 4 中的较小值 为 1, 起始位置要整除 对齐参数1,起始位置 1, 占 1 字节, 之后的起始位置 2;
3. short s, 对齐参数是 short 大小2 和 对齐数 4 中的较小值 为 2, 起始位置要整除 对齐参数2,起始位置 2, 占 2 字节, 之后的起始位置 4;
4. int i, 对齐参数是 int 大小4 和 对齐数 4 中的较小值 为 4, 起始位置要整除 对齐参数4,起始位置 4, 占 4 字节, 之后的起始位置 8;
计算大小 为 4 + 4 = 8;
最后要求 : 最终的大小必须是整除所有的对齐参数, 即 1 ,2, 和 4, 大小 8 满足要求 ;
最终的计算大小为 8;
*/
#pragma pack(4)
struct struct_3
{
char c;
char c2;
short s;
int i;
};
#pragma pack()
/*
内存对齐分析 : 这里注意与上面不同的是, 这里出现了一个 struct struct_3 类型,
结构体对齐参数 : 这里要注意结构体元素的对齐参数是该结构体元素中所有对齐参数的最大的一个, 不是结构体的大小;
1. char c, 对齐参数是 char 大小1 和 对齐数 4 中的较小值 为 1, 第一个起始位置没有要求 , 起始位置 0, 占 1 字节, 之后的起始位置 1;
2. char c2, 对齐参数是 char 大小1 和 对齐数 4 中的较小值 为 1, 起始位置要整除 对齐参数1,起始位置 1, 占 1 字节, 之后的起始位置 2;
3. struct struct_3 s, 对齐参数是 struct_3中 所有元素最大对齐数 4 和 对齐数 4 中的较小值 为 4, 起始位置要整除 对齐参数4,起始位置 4, 占 8 字节, 之后的起始位置 12;
4. int i, 对齐参数是 int 大小4 和 对齐数 4 中的较小值 为 4, 起始位置要整除 对齐参数4,起始位置 12, 占 4 字节, 之后的起始位置 16;
计算大小 为 12 + 4 = 16;
最后要求 : 最终的大小必须是整除所有的对齐参数, 即 1 和 4, 大小 16 满足要求 ;
最终的计算大小为 16;
*/
#pragma pack(4)
struct struct_4
{
char c;
char c2;
struct struct_3 s;
int i;
};
#pragma pack()
/*
struct struct_5 和 struct struct_4 结构体定义一样, 只是一个是 4 字节对齐, 一个是 8 字节对齐
gcc 默认 4 字节对齐, 其只支持 1, 2, 4 字节对齐, 不支持超过 4 的字节对齐
VC++ 默认 8字节对齐, 其可以支持 1, 2, 4, 8 字节对齐, 超过 8 的字节对齐 也不支持;
因此struct_5 虽然定义了 8 字节对齐, 但是编译器不支持, 即又默认成4 字节对齐, 这里 struct struct_5 和 struct struct_4 大小相同;
*/
#pragma pack(8)
struct struct_5
{
char c;
char c2;
struct struct_3 s;
int i;
};
#pragma pack()
int main()
{
printf("%ld, %ld, %ld, %ld\n", sizeof(struct struct_2), sizeof(struct struct_3), sizeof(struct struct_4), sizeof(struct struct_5));
return 0;
}
#运算符作用 :
#include
//作用 1 : 预处理器开始符号
//作用 2 : 将宏定义中的参数, 转换为字符串
#define CONVERS_STRING(str) #str
//实例 : 打印出调用的函数名称 和 结果
#define CALL(fun, num) ( printf ("函数名称 %s \n", #fun), fun(num) )
int square(int num)
{
return num * num;
}
int main()
{
printf("%s\n", CONVERS_STRING(Hello));
printf("%s\n", CONVERS_STRING(666));
printf("%s\n", CONVERS_STRING(main));
printf("调用函数 : %d\n", CALL(square, 9));
return 0;
}
# 运算符 将 Hello 666 main 转为 “Hello” “666” “main” 字符串, 将 square 转为了 “square” 字符串 ;
## 运算符作用 :
#include
//## 运算符 作用 : 预编译过程中 将两个符号连接在一起, 通常用于批量定义变量, 生成不同的变量名称
// 如定义 int 类型的 student1, student2 ... student9, 九个变量;
#define STUDENT(num) student_variable_##num
//定义结构体, 定义变量时时需要使用 struct student s1, 很麻烦
//简化结构体定义方案 1 : 使用 typedef struct _struct_name_ {} struct_name; 之后就可以使用 struct_name s1, 这样使用简便
typedef struct _student_struct_1_
{
int age;
int height;
}student_struct_1;
//简化结构体定义方案 2 : 定义下面的宏之后, 可以使用
#define STRUCT(type) typedef struct _tag_##type type;\
struct _tag_##type
STRUCT(student_struct_2)
{
int age;
int height;
};
int main()
{
//1. 定义变量示例
int STUDENT(1) = 1;
int STUDENT(2) = 2;
printf("%d, %d\n", STUDENT(1), STUDENT(2));
//2. 定义结构体常用方法
student_struct_1 s1;
s1.age = 18;
s1.height = 175;
printf("%d, %d\n", s1.age, s1.height);
//3. 使用 带 ## 运算符 的 宏定义 定义结构体方法
student_struct_2 s2;
s2.age = 19;
s2.height = 155;
printf("%d, %d\n", s2.age, s2.height);
return 0;
}