目录

一.指针

(一).二级指针

1.回忆:

2.二级指针的本质

(二).指针数组与数组指针

1.回忆

2.指针数组

3.字符指针数组

4.指针数组

(三).指针函数、函数指针

1. 指针函数

2.函数指针

3.函数指针数组

(四).指针总结

1.指针

2.地址

3.定义指针

4.地址类型

1)普通变量的地址

2)结构体变量的地址

3)数组的地址

4)函数的地址

5) 指针变量的地址

5.指针指向数组

二.linux标准main函数

三.数据类型

1.枚举enum

(1)定义:

(2)注意要点:

(3) 使用示例:

(4) 为什么使用枚举

2.共用体union (联合体)

(1)定义

(2)要点:

(3) 代码示例: 测试主机序

(4)共用体的作用

(5) 共用体对比结构体

3.位域

(1) 定义:

(2) 注意要点:

四.const

作用:

注意事项:

什么时候使用const?

1 修饰局部变量

2 函数形参

重点:

五.数据存储类型

1.extern

2.static

3.小结

六.内存分配

1.malloc(之前讲过)

2.realloc

3.calloc

4.memset和memcpy

七.带参宏

1.宏的特点:

2.宏的优势:

3.多参数的宏

4.带参宏 和 函数调用 的区别

八.条件编译

1.作用

2.#ifdef

3.#if #else #elif

4. #ifndef

九.类型重定义

1)为什么要类型重定义?

1.更有利于理解类型的含义

2.便于后期对类型修改

2)普通类型重定义

3)结构体类型重定义

4)实例

5)函数指针和数组指针类型

十.函数递归调用

1.函数入栈

2.要成功使用递归函数,需要注意的是:


一.指针

(一).二级指针

1.回忆:

指针?         :存放地址的变量。
变量的运算本质上是对变量中数据的运算。
地址有两种运算:
+ - 偏移运算
地址 +1
char 地址 +1 字节
int 类型 +4 字节 偏移运算得到的还是地址,地址类型不变
地址是有类型的。        什么类型的地址运算得到什么类型的对象        谁的地址运算得到谁
* 间接运算 一元运算,运算结果不是数值,是地址对应的那个对象。变量 数组 字符串常量
int a = 10;
*&a;// 运算得到变量 a ,和 a 的值没有关系。
printf("%d\n", *&a+1);// 输出 11 + 运算将变量 a 中的值取出进行运算,而不是间接运算得到的数值。
printf("%d\n", a+1);
定义指针变量:复合声明
int* p;
* 说明 p 是指针变量,由于指针变量存放地址,还需要说明存放地址的类型, int 说明 p 要存放 int 类型的地址。
用于定义的符号都不是运算符,而是类型。
* 指针类型
[] 数组类型
() 函数类型
int* a;
int b[10];
int c();

2.二级指针的本质

(二).指针数组与数组指针

1.回忆

指针和数组的关系
1. 数组名是数组首元素的地址,是元素类型的地址,是常量。
2. 元素在内存时连续的。

2.指针数组

复杂声明原则
在分析一个标识符的含义时,如果标识符被很多符号修饰,由近及远先右后左。
char* names [ 5 ];
[] 说明 names 是数组, char * 说明数组元素的类型
本质:
元素类型是指针的数组。
int *a[5]; |int *|int *|int *|int *|int *|
实例:(这里和二维数组一点关系都没有。)

3.字符指针数组

本质:数组元素是字符指针的数组。字符指针可以指向字符串常量。
定义: char *p[3]; p[0] p[1] p[2] |char *|char *|char *|

4.指针数组

地址是有类型的。
数组名:是首元素地址,是元素类型的地址,是常量,数组名是整个数组对象。
引例:
数组对象取地址得到数组类型的地址!
数组类型的地址间接运算得到数组对象(当成数组名用)。
#include <stdio.h>
int main ()
{
int a [ 5 ];
int* p = a ;
printf ( "%d %d %p %p\n" , sizeof ( a ), sizeof ( p ), a , p );
printf ( "%p %p\n" , a , a + 1 );
printf ( "%p %p\n" , & a , & a + 1 ); // 既然 a 是数组对象,就可以给数组对象取地址,偏移 20 字节
( *& a )[ 0 ] = 10 ; //*&a 运算得到数组对象,就是数组 a ,所以这里给 a[0] 赋值
return 0 ;
}
本质:
存放数组类型地址的指针是数组指针。
回忆:
二维数组的本质:元素类型是一维数组的一维数组。
由于使用一个指针指向数组时,指针要定义成数组的元素类型,因为数组名是元素类型的地址。
二维数组的数组名是一维数组类型的地址,所以使用指针指向二维数组我们需要定义数组指针

(三).指针函数、函数指针

1. 指针函数

本质:返回值是指针(地址)的函数就是指针函数。没有特性。
char * strcpy(char *dest,const char *src);
错误范例
我们要保证返回的地址在函数结束后还有效。
int* fun ()
{
int a ;
return & a ;
}

2.函数指针

本质:存放函数地址的指针。
函数的地址在内存的代码段。所以函数的地址没有间接运算,也没有偏移运算。
我们使用函数指针调用函数。
int get_sum ( int * p , int n );
int main ()
{
/*
p1 是指针, () 说明指向函数, int* int 是它指向函数的形参列表, int 是它指向函数的返回值类型
*/
int ( * p1 )( int* , int ); // 定义函数指针
p1 = get_sum ; // 函数名就是地址
p1 = & get_sum ; // 没有函数对象, &get_sum get_sum 是等价的
int a = 10 , b = 20 ;
p1 ( & a , b ); // 使用函数指针调用
( * p1 )( & a , b ); //*p1 不是运算得到函数对象, p1(&a, b) (*p1)(&a, b) 等价
}
函数指针的用处:
通过函数指针传递函数 ( 多态 )
多态:是传递逻辑。
C 语言使用函数指针实现多态

3.函数指针数组

本质:是数组,元素是函数指针。
为什么使用数组?需要若干个类型相同逻辑相似的变量。
使用函数指针数组,需要若干个类型相同,调用的逻辑相似的函数。(有点像多态)

(四).指针总结

1.指针

存放地址的变量。
对变量的运算都是对变量中数据的运算。         对指针变量的运算都是对地址的运算。

2.地址

计算机对内存的编址,字节( byte )为单位。
每个字节都有自己唯一的地址。(如果一个变量占多个字节,地址最小的那个字节的地址作为变量的地址。)
地址的运算
偏移运算 + -
地址 1+ 整数 = 地址 2
地址偏移一个单位,是偏移了一个类型长度。
整数 = 地址 2 - 地址 1 得到偏移的单位数
地址 1 = 地址 2 - 整数
地址是有类型的!!!!
间接运算 * 运算结果不是数值,而是地址对应的那个对象。变量,数组,字符串常量
int a = 10;
int* p = &a;
printf("%d\n", *p+1);//11 + 运算将变量 a 中的值取出和 1 进行相加,并不是 *p 运算得到 10

3.定义指针

定义的时候使用的符号都不是运算符
int* p;//* 说明 p 是指针变量,指针变量存放地址,地址是有类型的, int 说明 p 存放 int 类型的地址。
* 指针         [] 数组         () 函数         
int a;         
int*p = &a;         
p = &a;

4.地址类型

1)普通变量的地址

定义指针变量

int a; //&a int 类型的地址
char b;//&b char 类型的地址
int* p = &a;
char* pb = &b;
间接运算
得到变量
*p;// 运算得到 a
*pb;// 运算得到 b
2)结构体变量的地址
定义指针变量
间接运算
如何得到结构体类型的变量:     . 左值结构体变量 ;    -> 左值是结构体指针或者结构体地址
struct Student
{
        char name[20];
        int age;
};
struct Student a;
struct Student* p = &a;
p->name;// 运算得到变量 a 中的 name 成员,是一个数组对象 char[20]
(*p).name;//// 运算得到变量 a 中的 name 成员,是一个数组对象 char[20]
3)数组的地址

定义指针变量

int a[10];
a 是数组对象,也可以表示首元素地址,是 int 类型地址。
&a 得到数组类型的地址, int[10] 类型的地址
a &a 打印值虽然是一样的,但是地址的类型不一样。
*a 运算得到 a[0] int 类型变量 *&a 运算得到数组对象,是 int[10] 类型的对象
&(a+1) 语法错误!!! a+1 就是一个地址,地址不能取地址。
int(*p)[10] = &a;
间接运算
得到数组对象
int a[10];
int(*p)[10] = &a;
*p 运算得到 a ,是 int[10] 类型的对象
4)函数的地址

定义指针变量

int fun(char);
int(*p)(char) = fun;
p('a');
间接运算
没有间接运算和偏移运算。
5) 指针变量的地址

定义指针变量

int a;
int* p = &a;//&p int* 类型的地址
int**p2 = &p;
间接运算
*p2 运算得到 p ,是 int* 类型的对象

5.指针指向数组

使用指针变量指向一个数组,要存放首元素地址,是元素类型的地址
int a[10];
&a;//int[10] 类型的地址
&a+1;// 加了 40 个字节
&(a+1);// 报错,数组对象可以取地址,但是不能对地址取地址。
1 )指向一维数组
int a[10];// 元素是 int 类型
int* p = a;
2 )指向二维数组
int a[2][3];// 元素是 int[3] 类型
int(*p)[3] = a;
指针类型形参的写法
void fun ( int* p ) // 形参 p 是指针变量
{}
void fun ( int p []) // 形参 p 是指针变量
{}
void fun ( int p [ 10 ]) // 形参 p 是指针变量
{}
// 在函数的形参中无论如何定义不了数组, [] 就是和 * 等价

二.linux标准main函数

linux 系统中,允许 main 函数写参数(事实上就应该写参数)
形参只有一种写法
/*
char *argv[] 等价于 char** argv argument values 参数的值
argc argument count 参数个数
main 函数的实参是系统给传的,实参是一个字符指针数组
argv 是指正字符指针数组的指针
argc 是字符指针数组的长度
*/
int main ( int argc , const char * argv [])
{
return 0 ;
}

三.数据类型

基本数据类型: char int short long float double 整数类型可以使用 unsigned
构造数据类型: 指针。 数组。 结构体。 共用体 ( 联合体 ) 枚举。
空类型: void 用于函数返回值表示没有返回值 void* 类型不能使用间接运算

1.枚举enum

本质:是 int 常量。
给整型常量起名字

(1)定义:

//enum 是关键字, TimeofDay 是枚举类型的名字
enum TimeofDay
{
// 枚举的成员并不是被包含的关系,而是枚举类型的对象可能有的值
// 枚举的每个成员都是一个 int 常量
morning , // 0 0 起名叫 morning
afternoon , //1 0 起名叫 afternoon
evening //2
};
enum Week
{
one , //0
two , //1
three //2
};
// 不管枚举有多少成员它的大小都是 int 一样的。
sizeof ( enum TimeofDay ); //4
enum TimeofDay
{
morning = 100 , //100
afternoon , //101
evening //102
};
enum TimeofDay
{
morning = 100 , //100
afternoon = 99 , //99
evening , //100
afterevening = 100 , //100
};

(2)注意要点:

1 )枚举类型中,声明的第一个枚举成员默认值为 0
2 )以后每个枚举成员值将是前一个枚举成员的值加 1 得到的。
3 )定义枚举类型时,可以为枚举成员显式赋值。允许多个枚举成员有相同的值。
4 )没有显式赋值的枚举成员的值,总是前一个枚举成员的值 +1

(3) 使用示例:

enum TimeofToday
{
morning ,
halfmorninig = 0 ,
afternoon ,
halfafternoon = 10 ,
evening
};
void printTime ( enum TimeofToday tot ); // 在可读性上传达的信息是应该使用枚举的成员作为实参
//void printTime(int tot);// 给人的感觉实参可以是任意整数
int main ()
{
// 枚举的成员可以直接作为常量使用
printf ( "%d %d %d %d %d\n" , morning , afternoon , evening , halfmorninig , halfafternoon );
printTime ( 0 ); // 就算使用枚举类型作为形参,也可以直接使用整数作为实参
printTime ( moring );
printTime ( afternoon );
}
void printTime ( enum TimeofToday tot )
{
printf ( "%d\n" , tot );
}

(4) 为什么使用枚举

枚举能够表达出常量之间是有逻辑关系的,还能表达实参和形参的关系。
enum Directory
{
up ,
down ,
left ,
right
};
void printDir ( enum Directory dir );
int main ()
{
printDir ( up );
return 0 ;
}
void printDir ( enum Directory dir )
{
switch ( dir )
{
case up :
printf ( " \n" );
break ;
case down :
printf ( " \n" );
break ;
case left :
printf ( " \n" );
break ;
case right :
printf ( " \n" );
break ;
}
}

2.共用体union (联合体)

(1)定义

共用体的所有成员共用同一段内存,共用体占内存是最大基本类型成员的整数倍。
所有成员地址的值相同,都是共用体变量的地址。
union Data
{
int a ;
char b ;
};
sizeof ( union Data ); //4
union Data
{
int a ;
char b [ 18 ];
};
printf ( "%d\n" , sizeof ( union Data )); //20
#include <stdio.h>
union Data
{
int a ;
char b ;
};
int main ()
{
union Data d ;
printf ( "%d\n" , sizeof ( union Data )); //4
printf ( "%p %p %p\n" , & d . a , & d . b , & d ); // 所有成员的地址值是相等,而且和变量的地址是相等的
printf ( "%p %p %p\n" , & d . a + 1 , & d . b + 1 , & d + 1 ); //+4 +1 +4
return 0 ;
}

(2)要点:

-> 使用内存的方式,与结构体不同。
-> 共用体的各个成员共用内存,各个成员的起始地址是相同的。 ( 占内存小的成员,是在大成员的低位地址 )
-> 整个共用体占用的存储空间以长度最大的成员为准,共用体占内存是最大基本类型成员的整数倍。
-> 一个共用体变量,如果对多个成员赋值,会覆盖掉其他成员的数据。

(3) 代码示例: 测试主机序

字节序:大端对齐(低位地址存放高位数据)和小端对齐(低位地址存放低位数据)。
#include <stdio.h>
union Data
{
int a ;
char c ;
};
int main ()
{
union Data x ;
x . a = 0x12345678 ; //12 是高位数据 78 是低位数据
//x.c 是低位地址
printf ( "%x\n" , x . c ); //78 我们的虚拟机是小端
return 0 ;
}

(4)共用体的作用

1. 作为数据泛型,一个共用体变量可以表示多种类型的数据。
2. 实现巧妙的数据转换,比如将成员 a 转换为成员 b
#include <stdio.h>
#include <string.h>
struct Student//结构体类型,假设这个结构体是我程序中一个重要的类型,现在我要将它通过网络发送出去
{
char name[20];
int age;
};
union Data
{
struct Student stu;//结构体
char buf[sizeof(struct Student)];//和结构体一样大小的数组,这里的char表示一个字节的整数
};
int main()
{
union Data d;
strcpy(d.stu.name, "xiaoming");
d.stu.age = 18;//给联合的结构体成员赋值,本质上也是在给数组赋值
d.buf;//此时数组成员也是有数据的 这个数组可以直接使用write()通过网络发送出去
return 0;
}

(5) 共用体对比结构体

结构体每个成员都有独立的内存,而共用体所有成员共用同一段内存

3.位域

使用位域为了节省内存。         位域的成员不能取地址。

(1) 定义:

struct data
{
unsigned int a : 2 ; // 占一个 int 2 bit
unsigned int b : 4 ; // 接着 a 后面占一个 int 4 bit
unsigned int : 0 ; // 空域填满上一个 int 的剩余二进制位
unsigned int c : 3 ; // 另起一个 int ,占其中的 3 bit
}; // 这个结构体定义的并不节省内存,只是为了演示语法。
sizeof ( struct data ); //8
struct data
{
unsigned int a : 2 ;
unsigned int b : 4 ;
unsigned int c : 3 ;
unsigned int : 0 ;
};
sizeof ( struct data ); //4

(2) 注意要点:

1 )各位域必须存储在同一个类型长度中,不能跨两个类型长度。
2 )位域占用的位数,不能超过类型长度。
3 )允许位域无域名,这时它只用来作填充或调整位置。无名的位域是不能使用的。

四.const

作用:

将变量修饰成只读,就是不能修改。

注意事项:

const 修饰的变量一定要初始化。
const 是在编译是起作用。为了防止我们自己写代码的时候出错的。

什么时候使用const

1 修饰局部变量
当我们的函数中,或者某个结构中需要常量的时候,此时不适合使用宏定义,因为宏定义全局有效,一般是程序的各个地方都要使用。
此时可以使用 const 修饰一个局部变量
for(i = 0;i < n;i++)
{
const int studentNum = 40;
// 此时 studentNum 就相当于一个局部的常量。
}
2 函数形参
一般用来修饰指针
char* strcpy(char* dst, const char* src);
在函数里不能通过形参 src 修改作为实参的字符串
C 语言中, const 绝大部分情况下都是在形参中修饰常量指针。

重点:

常量指针 指针常量
常量指针:指针指向的对象不能改 *p 不能改
const int* p;
int const* p;
指针常量:指针不能改 p不能改        int * const p;
常量指针常量 p 和*p都不能改        const int* const p;
示例 3
#include <stdio.h>
int main ()
{
int a = 100 ;
const int * p = & a ; //p 是常量指针,指向 a
a = 80 ; //a 是可以修改的,因为 a 并没有被 const 修饰
* p = 60 ; // 报错,因为 p 是常量指针 *p 不能改
printf ( "%d\n" , a );
return 0 ;
}

五.数据存储类型

4 种。 自动 (auto) 、寄存器 (register) 、静态存储 (static) 、外部存储 (extern)
auto 局部变量, auto 关键字在 C 语言总几乎不用了。
register 如果一个变量需要高频的使用,应该被 register 修饰,会把变量定义在离 cpu 更近的位置,所以访问变量的效率会被提高。

1.extern

静态外部链接:使用其他文件中定义的全局变量

2.static

静态修饰
1 static 修饰全局变量 :
如果全局变量被 static 修饰,则该全局变量只能在当前文件中使用,不能被外部使用。
///// a.c ////
static int a = 200 ;
/////////// b.c ////////
#include <stdio.h>
extern int a ;
int main ()
{
printf ( "a = %d\n" , a ); // 报错,因为在 a.c a static 修饰了
return 0 ;
}
2 static 修饰局部变量
可将局部变量定义在静态区间,作用域不变,生命周期和程序一样长。
#include <stdio.h>
void fun ()
{
static int a = 90 ; // 此时这个变量不是调用函数时才创建,而是程序开始就创建,程序结束才释放
printf ( " %d \n" , a );
a ++ ;
}
int main ()
{
fun ();
//printf(" %d \n", a);// 报错,虽然生命周期延长了,但是还是局部变量,作用域没变。
fun ();
fun ();
return 0 ;}
输出 :90 90 90
修改后: 90 91 92
3 static 修饰函数:
被修饰的函数,限定在当前文件内使用。
/////a.c///////////////
static void fun ()
{
printf ( “hello world\n” );
}
/////////////////b.c///////////
void fun ();
int main ()
{
fun (); // 报错,因为静态函数在其他文件中不能使用
return 0 ;
}

3.小结

static const 经常在面试中被一起问到,但是他俩又没有任何关系。
static 修饰局部变量 生命周期变得和程序一样长,但是作用域不变
static 修饰全局变量 缩小了全局变量的访问范围,只能在定义全局变量的文件中使用全局变量,生命周期没有改变
static 修饰函数 缩小了函数的访问范围,只能在定义函数的文件中使用
const 将变量修饰成只读的。
常量指针 指向常量的指针, *p 不能变。 const * 左边
指针常量 不能改变指向的指针, p 不能变。 const * 右边

六.内存分配

1.malloc(之前讲过)

2.realloc

reset allocation
作用:改变原有堆空间变量的大小。如果原有的地址,足够改变空间,那么在原有地址基础上改变大小;如果原有地
址的空间不够大,会换一个新的地址。原有的数据拷贝到新的空间。旧空间自动释放。
参数 1 :原有空间的地址,地址必须是堆空间
参数 2 :新的大小
返回值:如果原有地址的空间足够扩张,会返回参数 1 ;否则返回新的地址。
void * realloc ( void * _Memory , size_t _NewSize )
#include <stdio.h>
#include <stdlib.h> // 头文件
int main ()
{
int i ;
int* pn = ( int* ) malloc ( 5 * sizeof ( int )); // 先申请 20 字节内存 5 int
if ( pn == NULL )
{
printf ( "malloc fail\n" );
return 0 ;
}
printf ( "malloc %p\n" , pn ); // 显示地址
for ( i = 0 ; i < 5 ; i ++ )
pn [ i ] = i ; // 给数组 5 个元素赋值 0~4
pn = ( int* ) realloc ( pn , 10 * sizeof ( int )); // 在原地址基础上扩张到 40 字节 10 int
if ( pn == NULL )
{
printf ( "realloc fail\n" );
return 0 ;
}
printf ( "realloc %p\n" , pn ); // 查看是否改变了地址
for ( i = 5 ; i < 10 ; i ++ )
pn [ i ] = i ; // 5~9 赋值 不管地址是否改变, 0~4 的数据都在
for ( i = 0 ; i < 10 ; i ++ )
printf ( "%3d" , pn [ i ]); // 输出
free ( pn ); // 只释放新的地址就行,就算原地址发生改变,也会被 realloc 自动释放掉
pn = NULL ;
return 0 ;
}

3.calloc

clear allocation
作用:将申请的空间全部清 0 malloc 申请的空间是不清零的。
参数 1 :申请单元个数
参数 2 :每个单元的大小
void * calloc ( size_t nmenb , size_t size );
#include<stdio.h>
#include<stdlib.h>
int main ()
{
int i ;
int* pn = ( int* ) calloc ( 10 , sizeof ( int )); // 申请 40 个字节, 10 int
for ( i = 0 ; i < 10 ; i ++ )
printf ( "%d " , pn [ i ]); // 保证内存都被初始化成 0
printf ( "\n" );
free ( pn ); // 释放内存
return 0 ;
}

4.memsetmemcpy

堆、栈、静态都可以用
memset 内存设置,给一段内存赋值
memcpy 内存拷贝,拷贝一段内存的值
#include <string.h> // 头文件
/*
参数 1 :要设置内存的地址,什么类型都可以
参数 2 :要给内存设置的值 ,每个字节都是参数 2 的值
参数 3 :内存长度,单位是字节
int a[10];
memset(a, 0, sizeof(a));// 给数组 a 全部清零
*/
void* memset ( void* ptr , int value , size_t num );
/*
参数 1 :向参数 1 的内存拷贝数据
参数 2 :被拷贝的内存
参数 3 :拷贝内存的长度
*/
void* memcpy ( void* dest , const void* src , size_t num );
#include <stdio.h>
#include <string.h>
int main() {]
int i, j;
// 示例1:字符串初始化
char str[] = "Hello";//数组长度6
memset(str, 'x', 3);//将str0~2设置成'x'
printf("%s\n", str);//xxxlo
// 示例2:数组清零
int nums[5] = {1, 2, 3, 4, 5};
memset(nums, 1, sizeof(nums));//整个数组全清,每个字节都是1 每个元素的值 0x01010101
for (i = 0; i < 5; i++)
printf("%#x ", nums[i]);//因为每个int占4个字节,所以每个元素的值都是 0x01010101
char map[8][6];
memset(map,'*', sizeof(map));//整个二维数组,每个字节都是'*'
for(i = 0;i < 8;i++)
{
for(j = 0;j < 6;j++)
{
printf("%c ", map[i][j]);
}
printf("\n");
}
return 0;
}
#include <stdio.h>
#include <string.h>
int main() {
// 示例1:复制数组
int src_arr[5] = {1, 2, 3, 4, 5};
int dest_arr[5];
memcpy(dest_arr, src_arr, sizeof(src_arr));//拷贝整个src_arr数组到dest_arr数组中
for (int i = 0; i < 5; i++)
printf("%d ", dest_arr[i]);//1 2 3 4 5
// 示例2:复制结构体
struct Point { int x; int y; };//在函数内定义的结构体类型,只能在函数内使用
struct Point src = {10, 20};//定义结构体对象初始化
struct Point dest;
memcpy(&dest, &src, sizeof(src));//将src对象数据拷贝到dest中
printf("(%d, %d)\n", dest.x, dest.y);
//事实上结构体可以struct Point dest = src; 我只想演示memcpy不挑类型
// 示例3:字符串复制(需手动补'\0')
char src_str[] = "Hello";
char dest_str[10];
memcpy(dest_str, src_str, strlen(src_str));
dest_str[strlen(src_str)] = '\0';//由于memcpy没有拷贝\0,因为strlen(src_str)不算\0,所以需要
手动在最后补\0
//memcpy(dest_str, src_str, strlen(src_str)+1);//这样就不需要手动补\0
printf("%s\n", dest_str);
//事实上拷贝字符串最好使用strcpy
return 0;
}

七.带参宏

1.宏的特点:

1 宏是在预处理阶段完成的事,完成了用宏值把宏名替换掉。
2 宏值是没有类型的。 宏只是简单的符号替换。无脑替。
3 宏名字一般用大写。为了和变量做以区分。
错误范例:
#include <stdio.h>
#define NUM 10;
int main ()
{
int i ;
for ( i = 0 ; i < NUM ; i ++ ) //for(i = 0;i < 10;;i++)
{
printf ( "hello\n" );
}
return 0 ;
}

2.宏的优势:

1. 一改全改
2. 使得常量有意义
实例:带参,给宏名加参数
#include <stdio.h>
// 宏名后面 () () 里面是宏的参数,可以不止一个,参数会被替换到宏值中
#define EXPAND(x) (10*(x))
int main ()
{
//(10*(2+1)) 2+1 是宏的实参,会被替换到宏值中
printf ( " %d \n" , EXPAND ( 2 + 1 )); //30
return 0 ;
}
不写括号的错误范例:
#define EXPAND(X) 1+10*X
int main ()
{
// 无脑替,直接使用宏值替换宏名,千万不要思考
//5*1+10*2+1
printf ( "%d\n" , 5 * EXPAND ( 2 + 1 )); //26
return 0 ;
}
使用带参宏替换一个函数示例:一般定义函数的逻辑相对固定,只是需要我们自己对函数起名字
#include <stdio.h>
//C 语言规定宏值必须写在同一行 所以宏值如果要多行写,需要加 \ 表示换行连接
#define POW(x) void x()\
{\
printf("hello world\n");\
}
POW ( hehe )
int main ()
{
hehe ();
return 0 ;
}

3.多参数的宏

#include <stdio.h>
// 多个参数使用 , 分隔
#define POW(x,y) ((x)*(y))
int main ()
{
//20 替换宏值中的 x 3 替换宏值中的 y
printf ( "%d\n" , POW ( 20 , 3 ));
return 0 ;
}

4.带参宏 和 函数调用 的区别

宏定义和函数是没有可比性的。
宏定义只是预处理阶段的宏值替换宏名。
函数是程序运行时代码的调用。
带参宏不会对参数类型进行检查。 只是替换,还没到编译的步骤。
带参宏不能定义返回值。 只是替换,根本不执行
带参宏不具备函数调用的灵活性。 只是替换,根本就不存在调用宏的说法
带参宏不执行任何逻辑,仅仅是在预处理阶段进行的代码替换。
带参宏仅仅能为我们编码时提供一些便利。
函数调用: 在程序执行之后才进行函数调用。 运行效率没有带参宏的效率高。因为执行阶段才调用。有类型检查,有作用域,有生命周期,有返回值。

八.条件编译

1.作用

if 结构有选择性的执行一部分代码。
条件编译是有选择性的编译一部分代码。
跨平台开发:同样的功能,不同的平台实现的方式可能不一样。
调试:一些输出调试信息的代码,可以使用条件编译,在发布用户版本的时候不编译用于调试的代码。

2.#ifdef

if define 如果定义了某个宏才对代码进行编译
用于 #ifdef 的宏,可以不写宏值
#include <stdio.h>
int main ()
{
#ifdef HELLO // 如果定义了 HELLO 这个宏,则编译下面两行代码,没定义 HELLO 宏就不编译
printf ( "hello world\n" );
printf ( "hello kitty\n" );
#endif // 条件编译到这里结束,下面代码到和条件编译没有关系
printf ( "hello farsight\n" ); // 这行代码无论如何都会被编译
return 0 ;
}
gcc -E test.c -o test.i
加宏之后
#include <stdio.h>
// 用于条件编译 ifdef 的宏,有没有宏值是无所谓的
#define HELLO
int main ()
{
#ifdef HELLO
printf ( "hello world\n" );
printf ( "hello kitty\n" );
#endif
printf ( "hello farsight\n" );
return 0 ;
}

3.#if #else #elif

示例 2:
使用 #else 可以实现两路分支的条件编译
#include <stdio.h>
#define DEBUG
int main ()
{
#ifdef DEBUG // 如果定义了 DEBUG 宏,会编译下面两行
printf ( "hello world\n" );
printf ( "hello farsight\n" );
#else // 如果没有定义 DEBUG 宏,会编译下面一行
printf ( "hello test\n" );
#endif // 条件编译范围到此为止
printf ( "hello kitty\n" ); // 这一行不受条件编译影响,无论如何都会编译
return 0 ;
}
#include <stdio.h>
// 用于多路分支的宏,必须有宏值,而且宏值是整数
#define TEST 1
int main ()
{
#if TEST == 1 // 如果 TEST 宏值是 1 则编译 下面 1
printf ( "hello world\n" );
#elif TEST == 2 // 如果 TEST 宏值是 2 则编译 下面 1
printf ( "hello test\n" );
#endif // 条件编译的范围到这里结束
printf ( "hello farsight\n" ); // 这一行不受条件编译影响,无论如何都会编译
return 0 ;
}

4. #ifndef

if not define
主要用于头文件,防止重复定义
示例 3:
///////person.h
#ifndef PERSON_H
#define PERSON_H
struct Person
{
char name [ 20 ];
int age ;
};
#endif
///////teacher.h
#include "person.h"
struct Teacher
{
struct Person p ;
char major [ 20 ];
};
////////student.h
#include "person.h"
struct Student
{
struct Person p ;
int score ;
};
////////main.c
#include "teacher.h"
#include "student.h"
int main ()
{
return 0 ;
}

main.c 在经过预处理后,会有两个 Person 结构体,所以提示 Person 结构体重复定义。

九.类型重定义

关键字 typedef
type define
给类型起别名。

1)为什么要类型重定义?

1.更有利于理解类型的含义

// unsigend int 起别名叫 student_num_t t type 缩写
typedef unsigend int student_num_t ;
void showStudents ( student_num_t stuNum );

2.便于后期对类型修改

//typedef char student_num_t;
typedef int student_num_t ;
student_num_t a ;
student_num_t b ;
student_num_t c ;
student_num_t d ;

2)普通类型重定义

typedef 只是起别名,原来的类型名还能用。
typedef int data_t ; // int 起别名叫 data_t
int main ()
{
data_t a = 10 ; // 等价于 int a = 10;
int b = 20 ; //int 类型还是可以使用的
printf ( "%d %d\n" , a , b ); //10 20;
return 0 ;
}
通常:
typedef char s8 ; //8 表示 8bit s 表示 signed 有符号 8 位整数
typedef unsigned char U8 ; //8 表示 8bit u 表示 unsigned 无符号 8 位整数
typedef short S16 ;
typedef unsigned short U16 ;
typedef int S32 ;
typedef unsigned int U32 ;
//STM32 单片机的 HAL 库, int8_t uint16_t

3)结构体类型重定义

struct student
{
char name [ 20 ];
int score ;
};
struct student s1 ;
struct student * p ;
为了便于开发者
typedef struct student stu_t ; // struct student 起别名叫 stu_t
stu_t s1 ; // 定义结构体变量
stu_t * p ; // 定义结构体指针变量

4)实例

给一个已经存在的结构体起别名
#include <stdio.h>
struct student
{
char name [ 20 ];
int score ;
};
typedef struct student stu_t ; // 给结构体 struct student 类型起别名叫 stu_t
int main ()
{
stu_t s1 = { "xiaoli" , 96 };
printf ( "%s, %d\n" , s1 . name , s1 . score );
return 0 ;
}
直接定义结构体时就 重命名,最常见的用法
#include <stdio.h>
typedef struct student
{
char name [ 20 ];
int score ;
} stu_t , * pstu_t ;
// 在定义结构体的时候直接起别名叫 stu_t pstu_t typedef struct student * pstu_t; 既结构体指针类型
int main ()
{
stu_t s1 = { "xiaoli" , 96 };
printf ( "%s, %d\n" , s1 . name , s1 . score );
//pstu_t 就是指针类型,所使用它定义指针变量不需要使用 *
pstu_t ps = & s1 ;
printf ( "%s, %d\n" , ps -> name , ps -> score );
return 0 ;
}
可以省略结构体名
#include <stdio.h>
// 省略了结构体本身的名字,只要别名
typedef struct
{
char name [ 20 ];
int score ;
} stu_t , * pstu_t ;
typedef struct
{
char name [ 20 ];
int age ;
} tea_t , * ptea_t ;
int main ()
{
stu_t s1 = { "xiaoli" , 96 };
pstu_t p = & s1 ;
printf ( "%s, %d\n" , p -> name , p -> score );
return 0 ;
}
注意:
struct
{
char name [ 20 ];
int score ;
} stu_t , * pstu_t ;
stu_t 就不是结构类型了,是结构体变量
pstu_t 是结构体指针变量
而且这个结构体类型除了以上两个变量,再也定义不了变量了,因为没有名字

5)函数指针和数组指针类型

#include <stdio.h>
typedef int ( * arr_ptr_t )[ 10 ]; //arr_ptr_t 是数组指针类型
typedef void ( * fun_ptr_t )( int ); //fun_ptr_t 函数指针类型
void fun ( int a )
{
printf ( "hehe\n" );
}
int main ()
{
int a [ 10 ];
arr_ptr_t p = & a ; //p 就是数组指针,存放 int[10] 类型地址的指针变量
fun_ptr_t p2 = fun ; //p2 是函数指针
return 0 ;
}

十.函数递归调用

1.函数入栈

调用形式:
void fun1 ()
{
fun1 (); // 严格来说这个不叫递归 因为这个函数只递 不 归
}
void fun2 ( int num )
{
printf ( "start:%d\n" , num );
if ( num == 5 )
return ;
fun2 ( num + 1 );
printf ( "fun %d finish\n" , num );
}
int main ()
{
fun2 ( 1 );
return 0 ;
}

2.要成功使用递归函数,需要注意的是:

一定要写明递的结束条件。 在满足结束条件后就不再递归了。
递归的结束条件,有时可以作为思考递归的一个逻辑入口。
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐