一、static
1.1 修饰函数内的局部变量:
void sayHi(void)
{
static int index = 5;
index++;
}
多次调用sayHi函数,index = 5 只有在第一次调用的时候初始化一次,后面的多次调用,此句话就不执行了。
1.2 修饰全局变量或函数:
#include <stdio.h>
static int count = 10; // 全局变量
static void write_eeprom(const U8* xdr); // 全局函数
当 static 修饰全局变量或函数时,会使之的作用域限制在声明它的文件内。
只有此文件内可以调用,外部文件不可访问。
二、extern
直译 外面的
extern int num; // 声明了一个整型变量num,它是在外面的、其他的文件中定义的。
执行此句代码时,不会为 num 分配任何存储空间,只是指示编译器 num 是在其他文件中定义的。
extern也可以用来修饰函数。
extern void write();
三、局部变量、全局变量
全局变量保存在内存的全局存储区中,占用静态的存储单元;
局部变量保存在栈中,只有在所在函数被调用时才动态地为变量分配存储单元。
默认值:
当局部变量被定义时,系统不会对其初始化,它的默认值有可能是一个垃圾值。
定义全局变量时,系统会自动对其初始化:
数据类型 | 初始化默认值 |
---|---|
int | 0 |
char | ‘\0’ |
float | 0 |
double | 0 |
pointer | NULL |
最佳实践是,无论是局部变量还是全局变量,定义的时候最好给显式地赋值一个默认值。
四、数组名
int heightArray[] = {1, 3, 4, 9}
heightArray既是数组名,也是指向数组第一个元素地址(也是数组的地址)的指针。
int* ptr = &heightArray[0];
也可以这么写
int* ptr = heightArray;
下面是我写的一个小程序,它本身没什么意义,单纯为了使大家更好的理解,数组名是个指针是什么意思:
int sumPreTwo(int* ptr);
int main() {
int heightArray[] = {1, 3, 4, 9};
int ret = sumPreTwo(heightArray);
printf("sum pre two, the result is %d", ret);
return 0;
}
int sumPreTwo(int* ptr)
{
int first = *ptr;
int* secondPtr = ptr + 1; // line2
int second = *secondPtr; // line3
return first + second;
}
输出:sum pre two, the result is 4
当然 line2和line3可以合并为一句
int second = *(ptr + 1)
由此,引发出求和的另一种写法:
#include <stdio.h>
int sum(int* ptr, int size);
int main() {
int heightArray[] = {1, 3, 4, 9};
int ret = sum(heightArray, 4);
printf("sum all, the result is %d", ret);
return 0;
}
int sum(int* array, int size)
{
int sum = 0;
for(int i=0; i<size; i++)
{
sum += *(array+i);
}
return sum;
}
/
输出: sum all, the result is 17
它本身也没什么意义,也是为了帮助大家更好地理解,为什么说:数组名是一个指向数组首元素的指针。
五、定义一个函数,返回值是一个数组
C 语言不允许返回一个完整的数组,只能返回一个指向数组的指针。
#include <stdio.h>
int[] genArray(int size);
int main() {
int heightArray[] = genArray(3);
for(int j=0; j<3; j++){
printf("%d element is: %d", j, heightArray[j]);
}
return 0;
}
int[] genArray(int size)
{
int arr[size];
for(int i=0; i<size; i++)
{
arr[i] = i;
}
return arr;
}
像上面这样的代码,不好意思,C不支持,编译就报错:
#include <stdio.h>
int* genArray();
int main() {
int* heightArrayPtr = genArray();
for(int j=0; j<3; j++)
{
printf("%d element is: %d \n", j, *(heightArrayPtr+j));
}
return 0;
}
int* genArray()
{
static int arr[3];
for(int i=0; i<3; i++)
{
arr[i] = i;
}
return arr;
}
六、静态数组
int staticArray[5]; // 静态数组声明
int staticArray[] = {1, 2, 3, 4, 5}; // 静态数组声明并初始化
获取数组长度:
int array[] = {1, 2, 3, 4, 5};
int length = sizeof(array) / sizeof(array[0]);
整个数组占用空间大小 / 一个元素占用空间大小 = 20字节 / 4字节 = 5个元素
七、动态数组(手动分配内存)
int size = 5;
int *dynamicArray = (int *)malloc(size * sizeof(int)); // 动态数组内存分配
// 使用动态数组
free(dynamicArray); // 动态数组内存释放
自己写了个例子:
#include <stdio.h>
#include <stdlib.h>
int* genArray(int size);
int main() {
int size = 5;
int* heightArrayPtr = genArray(size);
for(int j=0; j<size; j++)
{
printf("%d element is: %d \n", j, *(heightArrayPtr+j));
}
free(heightArrayPtr); // ※一定别忘了内存释放
return 0;
}
int* genArray(int size)
{
int* array = (int*)malloc(size * sizeof(int));
for(int i=0; i<size; i++)
{
array[i] = i;
}
return array;
}
//
0 element is: 0
1 element is: 1
2 element is: 2
3 element is: 3
4 element is: 4
malloc 的参数是字节数,此函数在 stdlib.h 中声明。
分配内存的函数,还有calloc,它们的区别是:
- malloc 只分配内存,并不初始化,默认值可能是一个垃圾值;
- calloc 分配内存,并初始化值为0;
- calloc 的参数是两个,元素数与每个元素占用的空间;
- 而malloc只有一个占用空间字节数参数;
它们在 stdlib.h中的声明如下:
void *__cdecl calloc(size_t _NumOfElements,size_t _SizeOfElements);
void __cdecl free(void *_Memory);
void *__cdecl malloc(size_t _Size);
八、枚举
enum DAY
{
MON=1, TUE, WED, THU, FRI, SAT, SUN
};
// 值 1 2 3 4 5 6 7
enum season {spring, summer=3, autumn, winter};
// 值 0 3 4 5
enum(枚举)类型的值只能是整数类型,包括int、short、char等。你不能直接在enum中指定浮点数(如float或double)作为其值。
如果想创建浮点型枚举,可以参考以下实现:
float floatValues[3] = {0.1, 0.2, 0.3};
enum {
FLOAT_VALUE_1,
FLOAT_VALUE_2,
FLOAT_VALUE_3
};
8.1 整数转换为枚举
#include <stdio.h>
enum day
{
saturday,
sunday,
monday
} workday;
int main()
{
int day1value = 1;
enum day weekend;
weekend = (enum day) day1value; //类型转换
// weekend = day1value; // 两种类型,不能直接赋值
printf("weekend: %d", weekend);
return 0;
}
九、空指针
int *ptr = NULL;
NULL 指向0x0系统保留的地址,应用程序不能访问。在程序中表达的意思是,此指针没有指向有效值(“没有初始化”)。
十、函数指针类型与变量
声明一个函数指针类型:
typedef int (*AddNum)(int);
typedef 表示我要定义一个类型。AddNum表示这个类型的名字。* 表示这是个函数指针类型。
int xxx(int)
这个范式,表达了定义的这个函数是什么样的函数,第一个int是返回值,第二个int是参数列表。xxx函数名是任意名,不是重点。
简单理解可以把它当成一个函数的模板,就是说我定义的这个函数指针类型,它指向的函数是一个怎样的函数:它的返回值是什么类型,它的参数列表是怎样的。
它定义了某种函数的模板,它代表了一堆这样的函数。
回顾一般指针的使用:
int a = 3;
int* ptr = &a;
要想定义一个指向整型的指针变量,首先要有整型这个类型(即 int),由于int 是C语言帮我们封装好了,所以可以直接用。
但是当要定义一个指向函数的指针变量时,并没有这个函数类型,所以才需要我们自己先声明一种函数类型。int xxx(int)
这个范式就描述了这种函数类型。
写了个例子,便于加深理解:
#include <stdio.h>
int addNum(int);
int ori = 0;
typedef int (*AddNum)(int); // 定义函数指针类型
void sayhello(AddNum);
int main() {
AddNum an = &addNum; // 创建一个函数指针变量,它指向了addNum函数
printf("hello!! %d \n", an(10)); // 调用函数指针
for(int i=0; i<3; i++)
{
sayhello(an);
}
return 0;
}
int addNum(int add)
{
ori += add;
return ori;
}
void sayhello(AddNum addNum)
{
printf("hello!! %d \n", addNum(1));
}
十一、共用体
union Data
{
int i;
float f;
char str[20];
};
Data中所有变量,共用一块内存地址。
i 占4字节,f 占4字节,str占20字节,所以Data变量占用空间为20字节(等于所有成员变量中占用空间最大的)。
int main( )
{
union Data data;
data.i = 10;
data.f = 220.5;
strcpy( data.str, "C Programming");
printf( "data.i : %d\n", data.i);
printf( "data.f : %f\n", data.f);
printf( "data.str : %s\n", data.str);
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
data.i : 1917853763
data.f : 4122360580327794860452759994368.000000
data.str : C Programming
可以看到,共用体的 i 和 f 成员的值有损坏。
因为共用内存地址,当写 str 值的时候,20个字节全重写了一遍,所以i、f 的值被覆盖掉了。
十二、typedef vs #define
它们有以下几点不同:
typedef 仅限于为类型定义符号名称,#define 不仅可以为类型定义别名,也能为数值定义别名,比如您可以定义 1 为 ONE。
typedef 是由编译器执行解释的,#define 语句是由预编译器进行处理的。
#define
在C语言中,#define
是一个预处理指令,用于在编译之前进行文本替换。预处理器会在编译之前处理源代码,根据#define
指令来替换代码中的文本。
例如,以下代码:
#define PI 3.14159
在编译之前,在代码中的任何地方出现的PI
,它都会被预处理器替换为3.14159
。
除了简单的文本替换,#define
还可以用于定义常量、宏函数等。例如:
#define SQUARE(x) ((x) * (x))
上述宏定义了一个名为SQUare
的宏函数,该函数接受一个参数x
并返回其平方。当你在代码中使用SQUare(5)
时,它会被预处理器替换为((5) * (5))
。
typedef
在C语言中,typedef
是一个关键字,它本身并不创建新的数据类型,只是为已有的数据类型创建了一个同义词或别名。
编译器在编译时解析typedef
声明,并在符号表中记录这个新的类型名称与其对应的基础类型。这样,在后续的代码中,每当编译器遇到这个新的类型名称时,它都会用基础类型来替换它。
例如,考虑以下typedef
声明:
typedef int Integer;
typedef struct {
int x;
int y;
} Point;
对于第一个typedef
,编译器会记住Integer
是int
的一个别名。在后续的代码中,任何使用Integer
的地方都会被当作int
来处理。
对于第二个typedef
,编译器会创建一个结构体类型,并为它分配一个别名Point
。这样,在代码中就可以使用Point
来声明该结构体的变量,而不需要每次都写出完整的结构体定义。
需要注意的是,typedef
的作用域是局部的,它遵循C语言的作用域规则。如果在一个函数内部使用typedef
定义了一个类型别名,那么这个别名只在该函数内部有效。在函数外部,编译器将不认识这个别名,除非在外部也有相应的typedef
声明。
十三、输入&输出
C 语言把所有的设备都当作文件
scanf() 函数用于从标准输入(键盘)读取并格式化, printf() 函数发送格式化输出到标准输出(屏幕)
#include <stdio.h> // 标准io库
int main()
{
printf("hello");
return 0;
}
stdio.h 是一个头文件 (标准输入输出头文件) and #include 是一个预处理命令,用来引入头文件。 当编译器遇到 printf() 函数时,如果没有找到 stdio.h 头文件,会发生编译错误。
printf() %d 匹配整型,%f 匹配float,%p 匹配指针,%s匹配字符串,%c匹配字符
getchar() & putchar() 函数
getchar()读取下一个可用的字符,并把它返回为一个整数。这个函数在同一个时间内只会读取一个单一的字符。
int putchar(int c) 函数把字符输出到屏幕上,并返回相同的字符。这个函数在同一个时间内只会输出一个单一的字符
gets() & puts() 函数
char *gets(char *s) 函数从 stdin 读取一行到 s 所指向的缓冲区,直到一个终止符或 EOF。
int puts(const char *s) 函数把字符串 s 和一个尾随的换行符写入到 stdout。
scanf() 和 printf() 函数
带格式的输入输出
#include <stdio.h>
int main( ) {
char str[100];
int i;
printf( "Enter a value :");
scanf("%s %d", str, &i);
printf( "\nYou entered: %s %d ", str, i);
printf("\n");
return 0;
}
十四、文件读写
#include<stdio.h>
int main()
{
FILE *f = NULL;
f = fopen("D:/2-Language/C/C-project1/tmp/test.txt", "w+");
fprintf(f, "Hello World! \n");
fputs("this is test for fputs...\n", f);
fclose(f);
}
int fputc( int c, FILE *fp ); // 写一个字符
int fputs( const char *s, FILE *fp ); // 写一个字符串
int fprintf(FILE *fp,const char *format, ...) // 把一个格式化字符串写入到文件中
int fgetc( FILE * fp ); // 读单个字符
char *fgets( char *buf, int n, FILE *fp ); // 读n-1个字符到buf,直到换行或文件结尾
int fscanf(FILE *fp, const char *format, ...) // 读到第一个空格或换行为止
十五、预处理器
C 预处理器不是编译器的组成部分,但是它是编译过程中一个单独的步骤。简言之,C 预处理器只不过是一个文本替换工具而已,它们会指示编译器在实际编译之前完成所需的预处理。我们将把 C 预处理器(C Preprocessor)简写为 CPP。
所有的预处理器命令都是以井号(#)开头。
#define 定义宏
#include 包含一个源代码文件
15.1 预定义宏
ANSI C 定义了许多宏。在编程中您可以使用这些宏,但是不能直接修改这些预定义的宏。
宏 | 描述 |
---|---|
__DATE__ | 当前日期,一个以 “MMM DD YYYY” 格式表示的字符常量。 |
__TIME__ | 当前时间,一个以 “HH:MM:SS” 格式表示的字符常量。 |
__FILE__ | 这会包含当前文件名,一个字符串常量。 |
__LINE__ | 这会包含当前行号,一个十进制常量。 |
15.2 预处理器运算符
宏延续运算符(\)
字符串常量化运算符(#)
十六、头文件
A simple practice in C 或 C++ 程序中,建议把所有的
常量、宏、系统全局变量和函数原型
写在头文件中,在需要的时候随时引用这些头文件。
引用头文件相当于复制头文件的内容
引用头文件会在预编译阶段,进行处理。
16.1 只引用一次头文件
如果一个头文件被引用两次,编译器会处理两次头文件的内容,这将产生错误。为了防止这种情况,标准的做法是把文件的整个内容放在条件编译语句中,如下:
#ifndef HEADER_FILE
#define HEADER_FILE
the entire header file file
#endif
这种结构就是通常所说的包装器 #ifndef。当再次引用头文件时,条件为假,因为 HEADER_FILE 已定义。此时,预处理器会跳过文件的整个内容,编译器会忽略它。
十七、错误处理
C 语言不提供对错误处理的直接支持,但是作为一种系统编程语言,它以返回值的形式允许您访问底层数据。在发生错误时,大多数的 C 或 UNIX 函数调用返回 1 或 NULL,同时会设置一个错误代码 errno,该错误代码是全局变量,表示在函数调用期间发生了错误。您可以在 errno.h 头文件中找到各种各样的错误代码。
所以,C 程序员可以通过检查返回值,然后根据返回值决定采取哪种适当的动作。
- perror() 函数显示您传给它的字符串,后跟一个冒号、一个空格和当前 errno 值的文本表示形式。
- strerror() 函数,返回一个指针,指针指向当前 errno 值的文本表示形式。
#include <stdio.h>
#include <errno.h>
#include <string.h>
extern int errno ;
int main ()
{
FILE * pf;
int errnum;
pf = fopen ("unexist.txt", "rb");
if (pf == NULL)
{
errnum = errno;
fprintf(stderr, "错误号: %d\n", errno);
perror("通过 perror 输出错误");
fprintf(stderr, "打开文件错误: %s\n", strerror( errnum ));
}
else
{
fclose (pf);
}
return 0;
}
///
错误号: 2
通过 perror 输出错误: No such file or directory
打开文件错误: No such file or directory
17.1 程序退出状态
exit(EXIT_SUCCESS);
exit(EXIT_FAILURE);
<stdlib.h>
#define EXIT_SUCCESS 0
#define EXIT_FAILURE 1
十八、可变参数(不常用,了解下就行)
int func_name(int arg1, ...);
#include <stdio.h>
#include <stdarg.h>
// 定义一个可变参数函数
void print_numbers(int count, ...) {
va_list args; // 定义一个va_list类型的变量
va_start(args, count); // 初始化args,使其指向第一个可变参数
for (int i = 0; i < count; i++) {
int value = va_arg(args, int); // 获取下一个整数参数
printf("%d ", value);
}
va_end(args); // 清理va_list变量
}
int main() {
print_numbers(3, 1, 2, 3); // 输出: 1 2 3
print_numbers(5, 4, 5, 6, 7, 8); // 输出: 4 5 6 7 8
return 0;
}
使用va_list、va_start、va_arg和va_end宏来处理可变参数。这三个宏,在<stdarg.h>中定义。
十九、内存分配
序号 | 函数 | 描述 |
---|---|---|
1 | void *calloc(int num, int size); | 在内存中动态地分配 num 个长度为 size 的连续空间,并将每一个字节都初始化为 0。所以它的结果是分配了 num*size 个字节长度的内存空间,并且每个字节的值都是 0。 |
2 | void free(void *address); | 该函数释放 address 所指向的内存块,释放的是动态分配的内存空间。 |
3 | void *malloc(int num); | 在堆区分配一块指定大小的内存空间,用来存放数据。这块内存空间在函数执行完成后不会被初始化,它们的值是未知的。 |
4 | void *realloc(void *address, int newsize); | 该函数重新分配内存,把内存扩展到 newsize。 |
void * 类型表示未确定类型的指针。C、C++ 规定 void * 类型可以通过类型转换强制转换为任何其它类型的指针。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
char name[100];
char *description;
strcpy(name, "Zara Ali");
/* 动态分配内存 */
description = (char *)malloc( 30 * sizeof(char) );
if( description == NULL )
{
fprintf(stderr, "Error - unable to allocate required memory\n");
}
else
{
strcpy( description, "Zara ali a DPS student.");
}
/* 假设您想要存储更大的描述信息 */
description = (char *) realloc( description, 100 * sizeof(char) );
if( description == NULL )
{
fprintf(stderr, "Error - unable to allocate required memory\n");
}
else
{
strcat( description, "She is in class 10th");
}
printf("Name = %s\n", name );
printf("Description: %s\n", description );
/* 使用 free() 函数释放内存 */
free(description);
}
输出:
Name = Zara Ali
Description: Zara ali a DPS student in class 10th
strcpy( description, "Zara ali a DPS student."); // copy
strcat( description, "She is in class 10th"); // 拼接
C 语言中常用的内存管理函数和运算符
malloc() 函数:用于动态分配内存。它接受一个参数,即需要分配的内存大小(以字节为单位),并返回一个指向分配内存的指针。
free() 函数:用于释放先前分配的内存。它接受一个指向要释放内存的指针作为参数,并将该内存标记为未使用状态。
calloc() 函数:用于动态分配内存,并将其初始化为零。它接受两个参数,即需要分配的内存块数和每个内存块的大小(以字节为单位),并返回一个指向分配内存的指针。
realloc() 函数:用于重新分配内存。它接受两个参数,即一个先前分配的指针和一个新的内存大小,然后尝试重新调整先前分配的内存块的大小。如果调整成功,它将返回一个指向重新分配内存的指针,否则返回一个空指针。
sizeof 运算符:用于获取数据类型或变量的大小(以字节为单位)。
指针运算符:用于获取指针所指向的内存地址或变量的值。
& 运算符:用于获取变量的内存地址。
- 运算符:用于获取指针所指向的变量的值。
-> 运算符:用于指针访问结构体成员,语法为 pointer->member,等价于 (*pointer).member。
memcpy() 函数:用于从源内存区域复制数据到目标内存区域。它接受三个参数,即目标内存区域的指针、源内存区域的指针和要复制的数据大小(以字节为单位)。
memmove() 函数:类似于 memcpy() 函数,但它可以处理重叠的内存区域。它接受三个参数,即目标内存区域的指针、源内存区域的指针和要复制的数据大小(以字节为单位)。
本文完。