C语言存储类别、链接和内存管理
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>
#include "diceroll.h" // 包含的头文件如果是C标准库的,需要使用<>告诉编译器去标准库中查找,如果是用户定义的,需要使用""告诉编译器去IDE指定的用户存放头文件的区域查找
int mode; // 将变量定义在函数外面,变量具有文件作用域,变量mode对当前翻译单元(当前文件和include包含的所有文件的总和称为翻译单元)内,当前声明语句之后的所有函数可见
double distance; // 变量distance可以描述为文件作用域,静态存储期(在编译时分配内存,并在程序运行过程中一直保留这块内存),外部链接(在当前程序的其他翻译单元中可以引用该变量,翻译单元即由.c源文件和文件中包含的头文件等的总和,程序如果由多个源文件组成会形成多个翻译单元),静态变量如果没有显式初始化则会将所有位初始化为0(在某些硬件系统中浮点数的0不是所有位都为0)
static double fuel; // 对文件作用域的变量使用static修饰,将变量定义为内部链接,内部链接只对当前翻译单元可见,其他翻译单元不能引用
extern int sets; // 使用extern,引用式声明,告诉编译器去其他地方(包括当前文件的上下文)寻找sets,这里sets的定义式声明在别的翻译单元中,当前翻译单元想要使用同一个变量则必须要extern引用
void critic(void); // 函数原型默认extern引用,函数也具有文件作用域
void critic2(int* ptr); //函数原型作用域用于函数原型中的形参/变量名,作用域范围从定义处到原型声明结束,所以除了变长数组之外,形参名可以省略,简写为void critic2(int*); 函数定义时必须有形参名,且函数定义的形参名可以和函数原型中的不同
void set_mode(int p_mode);
void get_info(void);
void show_info(void);
int invoke_count(void);
void random_array(void);
void array_sort(int* arr, int len);
void test_random(void);
static void dice_game(void); // 将函数声明为内部链接,文件/翻译单元私有
int* dynamic_make_array(int size, int init);
void dynamic_show_array(int* arr, int size);
void words(void);
void others(void);
int main(void)
{
words();
return 0; //C规定main()函数的return 0和exit(EXIT_SUCCESS)作用相同,0代表成功运行
}
void critic(void)
{
auto int units = 0; // 一般在花括号{}内声明的变量和函数形参内的变量都默认auto,自动存储期,指程序在执行到当前函数才会分配内存给变量(存储在栈中),并且在函数结束时释放内存,当前变量的标识符为units,程序通过该标识符访问内存块,在C语言中这个内存块称为对象(不同于面向对象语言的对象),变量units可以被描述为自动存储期、块作用域(花括号{}内的区域被称为块)、无链接(块作用域以外不可见,除了传参/返回之外无法引用)
//虽然units的内存在程序执行到当前函数时就已分配,但只对当前块内在units的声明语句下面的区域可见,自动存储器的变量在声明时如果不手动进行初始化则该内存块仍然保留着之前该区域的垃圾值
printf("How many pounds to a firkin of butter?\n");
scanf("%d", &units);
while (units!=56)
{
critic2(&units);
}
printf("You must have looked it up!\n");
}
void critic2(int* ptr) { // 函数头的普通变量也是自动存储期、块作用域、无链接
printf("No luck, my friend. Try again.\n");
scanf("%d", ptr);
} // 离开critic2函数会释放变量ptr的内存,即&ptr地址的内存块,这不影响主调函数的&units
void set_mode(int p_mode) {
// 如果形参也命名为mode会盖住前面定义的静态变量mode,使静态变量mode对当前函数不可见
extern int mode; // 引用式声明,静态变量mode对当前翻译单元所有函数可见,所以不需要这一句声明也可以调用变量,引用式声明不可以初始化,只有定义式声明可以初始化
switch (p_mode)
{
case 1:
mode = 1;
break;
case 0:
mode = 0;
break;
default:
printf("Invalid mode specified. Mode %s used.\n", mode ? "1(US)" : "0(metric)");
}
}
void get_info(void) {
printf("Enter distance traveled in %s:", mode ? "miles" : "kilometers");
scanf("%lf", &distance);
printf("Enter fuel consumed in %s:", mode ? "gallons" : "liters");
scanf("%lf", &fuel);
}
void show_info(void) {
if(mode)
printf("Fuel consumption is %.2f miles per gallon.\n",distance / fuel);
else
printf("Fuel consumption is %.2f liters per 100 km.\n", fuel / distance * 100);
}
int invoke_count(void) { // 记录函数被调用的次数
static int invoke_c; // 声明静态存储期的块作用域无链接变量,静态变量invoke_c会在编译阶段分配内存,在程序运行时一直存在,所以实际函数调用时会跳过这个语句,静态变量在程序一开始执行就存在,没有显式初始化则初始化为0
invoke_c++; //静态变量在程序运行时一直存在,所以该值会不断递增。块作用域使其只对当前函数可见,函数私有,无链接使其不能被extern引用
return invoke_c;
}
void random_array(void) {
int arr[100];
srand(time(0)); //time()函数在time.h头文件中,传入指向time_t类型的指针,返回当前时间戳并将该值也存到传入的指针指向的对象上,传入NULL或0省略指针赋值,srand()接收unsigned int类型作为随机数的种子,传入time_t类型可能发生截断
for (int i = 0; i < 100; i++) //for循环的()为函数的语句块的子块,在子块中声明的变量的作用域也为子块,所以for循环结束之后会释放i的内存
{ // for循环的循环体{}为for循环的()的子块,如果在{}内再声明一个int i,会盖住()中i,当单次循环结束离开{}进入()判定时依然是以()中声明的i来判定
arr[i] = rand() % 10 + 1; //rand()生成随机数,范围0-RAND_MAX,%10是0-9,+1是1-10
}
array_sort(arr, 100);
for (int i = 0; i < 100; i++)
{
printf(" %d,", arr[i]);
}
putchar('\n');
}
void array_sort(int* arr, int len) { // 使用归并排序
if (len<2)
return;
int mid = len / 2;
array_sort(arr, mid);
array_sort(arr+mid, len-mid);
int* ptr = (int*)malloc(len * sizeof(int)); // malloc(size_t size)函数动态分配内存,即在程序运行中,在执行到malloc()函数时根据传参的大小分配内存块,返回内存块的地址,类型为void*即指向void的指针,该类型相当于一个通用指针,通过(int*)强转,动态分配的内存需要通过free()函数来释放,不会自动释放
// 通过len*sizeof(int)生成存放len个int元素的空间
// malloc()如果分配内存失败,会返回NULL
if (ptr==NULL)
{
printf("动态分配失败");
exit(EXIT_FAILURE);
}
int r, l,i;
for ( i=l = 0,r=mid; l < mid&&r<len;)
ptr[i++] = arr[l] > arr[r] ? arr[l++] : arr[r++];
while (l<mid)
ptr[i++] = arr[l++];
while (r<len)
ptr[i++] = arr[r++];
for ( i = 0; i < len; i++)
arr[i] = ptr[i];
free(ptr); //free()只能释放动态分配的内存,自动存储期和静态存储期都不行
}
void test_random(void) {
int count[11]={0}; // 只显式初始化一个值,剩下的元素都初始化为0
for (int i = 0; i < 10; i++)
{
srand(time(0)+i*i);
for (int j = 0; j < 1000; j++)
{
count[rand() % 10 + 1]++; //count[1]对应随机数1,count[10]对应随机数10,每次出现哪个随机数就把哪个计数器递增
}
printf("loop %d:\t", i);
for (int j = 1; j < 11; j++)
{
printf("count[%d]=%d\t", j, count[j]);
count[j] = 0;
}
putchar('\n');
}
}
int roll_n_dice(int dice, int sides) { // 抛多个骰子,dice接收骰子的个数,sides指定骰子有几面
int n, sum;
sum = n = 0;
putchar('(');
for (int i = 0; i < dice; i++)
{
n = rand() % sides + 1;
printf("%d ", n);
sum += n;
}
printf(") = %d\n", sum);
return sum;
}
void dice_game(void) {
int dice, sides;
fputs("Enter the number of sets(1-64); enter q to stop:", stdout);
while (scanf("%d",&sets)==1&&sets>0&&sets<65)
{
srand(time(0));
printf("How many sides(2-64) and how many dice(1-64)?");
if (scanf("%d%d",&dice,&sides)!=2||dice<0||dice>64||sides<2||sides>64)
{
puts("invalid input. sides(2-64) dice(1-64).");
}
else
{
printf("Here are %d sets of %d %d-sided throws.\n", sets, dice, sides);
for (int i = 0; i < sets; i++)
{
roll_n_dice(sides, dice);
}
}
fputs("How many sets(1-64)? Enter q to stop:", stdout);
}
}
int* dynamic_make_array(int size, int init) {
int* ptr = (int*)calloc(size, sizeof(int)); // calloc()函数也用于动态分配内存,第一个参数指定元素的数量,第二个参数指定元素的大小,函数会将所有位初始化为0,返回void*需要强转
if(ptr) //动态分配内存如果失败会返回NULL
for (int i = 0; i < size; i++)
ptr[i] = init;
return ptr;
}
void dynamic_show_array(const int* arr, int size) {
// 指针arr是其指向对象的在当前函数内的初始方式,如果同时也是唯一的方式(即函数内没有用其他指针也指向该对象),可以使用restrict关键字修饰arr,restrict关键字用于编译器优化,告诉编译器当前声明的指针是访问数据对象唯一且初始的方式,restrict关键字只能用于指针
for (int i = 0; i < size; i++)
{
printf("%d ", arr[i]);
if (i % 8 == 7)
putchar('\n');
}
}
void words(void) {
fputs("How many words do you wish to enter?", stdout);
int n;
if (scanf("%d", &n) == 1 && n > 0)
{
char** words = (char**)malloc(n * sizeof(char*)); // 创建指向字符串的指针数组,可以想象成:char是字符,char*是字符串/字符数组,char**是字符串数组/指向字符串的指针数组,所以每个元素为指针大小,有n个元素
if (words == NULL)
goto end; // goto语句的标签是函数作用域,即使一个标签首次出现在函数的内层块中,它的作用域也延伸至整个函数
printf("Enter %d words now:\n", n);
char temp[20];
for (int i = 0; i < n; i++)
{
scanf("%19s", temp); // 最多读19个字符,末尾留一位\0
words[i] = (char*)malloc((strlen(temp) + 1) * sizeof(char)); // 先存放到临时数组
strcpy(words[i], temp); //字符串赋值
}
puts("Here are your words:");
for (int i = 0; i < n; i++)
{
printf("%s ", words[i]);
free(words[i]); // 每个动态分配的内存分别释放
}
free(words); // 千万记得释放
}
end:; //goto的end标签,对应的语句为;空
}
void others(void) {
register int i; // register关键字,请求将声明的变量储存在CPU的寄存器中,与普通变量相比,寄存器变量访问、处理的速度更快,无法获得寄存器变量的地址,所以无法使用&取址,编译器视情况决定是否将该变量储存到寄存器中,即便储存在了内存中也不能使用&
const volatile char* ptr; //volatile易变的,volatile限定符告知计算机,代理(而不是变量所在的程序)可以改变该变量的值,通常被用于硬件地址以及在其他程序或同时运行的线程中共享数据,编译器会根据volatile改变优化,const和volatile顺序无所谓
//_Thread_local //以关键字 _Thread_local声明具有线程存储期的对象,每个线程都获得该变量的私有备份,线程存储期从被声明时到线程结束一直存在
//_Atomic int hogs; // _Atomic原子类型变量,并发程序通过各种宏函数访问原子类型,当一个线程对一个原子类型的对象执行原子操作时,其他线程不能访问该对象
//static int j; int k; malloc(sizeof(int)); //静态变量、自动变量、动态变量分别存储在内存中的三个不同的区域
int arr1[10];
int arr2[10]={1,2,3,4,5,6,7,8,9,0};
memcpy(arr1, arr2, sizeof(arr2)); //内存字节拷贝,参数1、2和返回值(返回参数1)都是void*类型,参数3指定字节数,参数1和参数2不应重叠
memmove(arr1, arr2, sizeof(arr2)); //和memcpy区别是参数1、2可以重叠,使用缓冲区进行拷贝,src拷贝到缓冲区再拷贝到dest
/*存储期分为静态、自动、动态。静态存储期在程序开始执行时分配内存。自动存储期在程序进入变量定义所在的块时分配变量内存,离开块释放内存。动态存储期在调用malloc()calloc()分配内存,调用free()释放
作用域分为文件作用域、块作用域、函数作用域。
链接分为外部链接、内部链接、无链接。*/
}
//void func(int a1[const], int a2[restrict], double ar[static 20]) //C99规定,int* const a1等价int a1[const],int* restrict a2等价int a2[restrict],double ar[static 20]告诉编译器该数组至少有20个元素

