浅学C语言

本文介绍了C语言的基础知识,包括预处理指令、数据类型、函数使用、输入/输出操作、指针的概念和操作、内存管理中的动态分配以及结构体的使用。还讨论了编译过程、文件输入输出和位操作。内容涵盖了C语言的核心概念,适合初学者学习。
摘要由CSDN通过智能技术生成

本文是阅读《C Primier Plus(第6版)》时的一些笔记,仅作记录
PDF文件网盘链接:https://pan.baidu.com/s/1gyR0xmNStsE8MaWI2mi4Hw?pwd=6999


1 C概述

C语言在B语言的基础上进行设计而来,是一门面向过程的、抽象化的通用程序设计语言,广泛应用于底层开发。

  • C标准
    美国国家标准协会(ANSI) 1989/1990——C89/C90; 1994——C99; 2007——C11

  • 编程的7个步骤

    1. 定义程序的目标
    2. 设计程序
    3. 编写代码
    4. 编译程序
    5. 运行程序
    6. 测试和调试程序
    7. 维护和修改
  • C程序源文件:一个项目中包含main()函数的.c文件,是整个编译过程的开始

  • 编译过程:

    1. 预处理:.c/.cpp→.i
    2. 编译: .i文件–>.s文件
    3. 汇编: .s文件–>.o文件
    4. 链接: .o文件–>bin文件

其他关于GNU的gcc、g++编译器、Linux OS编译命令见:https://blog.csdn.net/Morejay/article/details/128515728

#include "stdio.h"  // C程序的 标准输入输出 头文件

// C常用IO函数
int getchar()//从标准输入设备写入一个字符
int putchar()//向标准输出设备读出一个字符
int scanf(char* format[, argument…])//从标准输入设备读入格式化后的数据
int printf(char* format[, argument…])//向标准输出设备输出格式化字符串
char* gets(char* string)//从标准输入设备读入一个字符串
int puts(char* string)//向标准输出设备输出一个字符串
int sprintf(char* string, char* format[,])//把格式化的数据写入某个字符串缓冲区

引入头文件使用""<>的区别

  1. 引用的头文件不同
    #include <>引用的是编译器的类库路径里面的头文件。
    #include ""引用的是用户当前程序目录的相对路径中的头文件。
  2. 用法不同
    #include <>用来包含标准头文件(例如stdio.hstdlib.h).
    #include ""用来包含非标准头文件。
  3. 调用文件的顺序不同
    #include <>编译程序会先到标准函数库中调用文件。
    #include ""编译程序会先从当前目录中调用文件。
  4. 预处理程序的指示不同
    #include <>指示预处理程序到预定义的缺省路径下寻找文件。
    #include ""指示预处理程序先到当前目录下寻找文件,再到预定义的缺省路径下寻找文件。

简单C程序示例:

#include <iostream>  // iostream是C++输入输出流类,兼容C和C++,C程序适用stdio.h头文件即可
using namespace std;  // CPP程序的标准输入输出 命名空间,可以不使用,否则下面代码要修改如下:
// std::cout << "C++ ..." << std::endl;

int avoid_conflict_modify_main() {
	printf("c ...\n");
	cout << "C++ ..." << endl;

	int num;  // 变量声明
	num = 6;  // 变量赋值

	printf("my favorite number is %d .", num);

	getchar();  // 该行读取一次键的按下,以保障程序暂停,不会出现程序窗口一闪而过的情况

	return 0;
}

2 数据和C

  • 关键字:int、short、long、unsigned、char、float、double、【C99】_Bool(0 or 1 只占一位存储空间)、_Complex(复数)、_Imaginary(虚数)
  • 运算符:sizeof()
  • 函数:scanf()
  • 整数类型和浮点数类型的区别
  • 如何书写整型和浮点数类型,如何声明这些类型的变量
  • 如何使用printf()和scanf()函数读写不同类型的值

通过C程序了解上面的概念:

注意下面的代码中在strcpy()scanf()函数名加入_s
在适用Visual Studio编译C程序时,strcpy() scanf()会报错?????

  • 解决方案:
  1. 使用在方法后加 _s—— VS编译器提供的函数,非C语言库提供的标准函数 (不建议,只适用VS编译器)
  2. 在代码的最顶端输入#define _CRT_SECURE_NO_WARNINGS
  3. 在VS的界面中,最顶端找到项目,点击属性,找到C/C++这一栏,选择预处理器,打开预处理器定义这一栏最右边的小三角,选择编辑。在最上方的白框中输入_CRT_SECURE_NO_WARNINGS,点击确定,然后点击应用
#include <stdio.h>

void differentDataTypeOutput(); // 函数定义

int avoid_conflict_2_main() {
	float weight;  // float精度高于int  高精度向低精度转换可能会丢失数据(double -> int)
	float value;

	// 获取用户的输入
	scanf_s("%f", &weight);  // &weight把输入的值赋给名为weight的变量  &用于找到变量的地点

	value = 1700.0 * weight * 14.5833;

	printf("weight is worth $ %.2f.\n", value);  // .2f用于精确控制输出,指定输出浮点数只显示小数点后两位

	// %d--显示整型;%o--八进制(0oo,o:0~7);%x--十六进制(0xhh、0Xhh,h:0~f)
	differentDataTypeOutput();

	return 0;
}

void differentDataTypeOutput() {
	// 个人计算机常见:long long占64位;long占32位;short占16位;int占16或者32位
	unsigned int un = 3000000000;  /* int为32位和short位16位的系统*/
	short end = 200;
	long big = 65537;
	long long verybig = 12345678908642;

	printf("un = %u and not %d\n", un, un);
	printf("end = %hd and not %d\n", end, end);  // 给函数传参时,编译器把short的值自动转换成int
	printf("big = %ld and not %hd\n", big, big);
	printf("verybig = %lld and not %ld\n", verybig, verybig);
}

C99新增两个头文件stdint.intinttypes.h,以确保C语言的类型在各系统中的功能相同

int32_t表示32位的有符号整数类型,使用32位int的系统中,头文件会把int32_t作为int的别名

float some;
some = 4.0 * 2.0// 编译器默认假定浮点型常量为(64位)double类型,使用双精度进行乘法运算,然后将乘积截断成float
// 这样做的计算精度更高,但是会减慢程序的运行速度

在浮点型常量后加forF可以覆盖默认设置。编译器会将其看作float类型

4.0f  // float类型
4.0L or 4.0l // long double

printf()函数打印浮点值——%e打印指数计数法的浮点数

C99:系统支持十六进制格式的浮点数,可以用aA分别代替eE


浮点值的上溢和下溢

  • 计算导致数值过大,超过当前类型所能表达的范围时,发生上溢:赋无穷大printf()显示 infinfinity
  • 一个很小的数,假设指数已经是最小值了,如:0.1234E-10再除以10,得到结果0.0123E-10,虽然得到了结果但是末尾损失了有效数字——下溢
  • 特殊浮点值:NaN(Not a Number)

C语言的3种复数类型:float_Complexdouble_Complexlong double_Complex
C语言的3种虚数类型:float_Imaginarydouble_Imaginarylong double_Imaginary

需要包含complex.h头文件,便可以用complex代替_Complex,用Imaginary代替_Imaginary,用I代替-1的平方根

字符串类型string

#include <stdio.h>
#include <string.h>  // 字符串库函数,提供strlen()函数的原型

#define DENSITY 62.4  // 预处理指令,末尾无';' 定义人体密度(磅/立方英尺),编译时会将 DENSITY 替换成 62.4
/* 也可以是适用 float density = 62.4; 但是这样做程序可能会在无意间改变它的值*/

int avoid_3_main() {
	float weight, volume;
	int size, letters;
	char name[40];  // 可容纳40个字符的数组,C语言没有专门用于存储字符串的变量类型,C语言字符串以'\0'结尾

	printf("what's your name?\n");
	//strcpy(name, "moonjay");
	scanf("%s", name);
	printf("%s, what's your weight in pounds?\n", name);
	scanf("%f", &weight);

	size = sizeof name;
	letters = strlen(name);
	volume = weight / DENSITY;

	printf("well, %s, your volume is %2.2f ciubic feet.\n", name, volume);
	printf("your name has %d letters, and we have %d bytes to store it.\n", letters, size);

	return 0;
}

void explain_const() {
	// C90
	// 
	
}

C90标准新增了const关键字,用于限定一个“变量”为只读
上面代码中预定义的 DENSITY 属于常量,即符号常量

const int MONTHS = 12; // MONTHS在程序中不可更改

/* C头文件 limits.h和float.h分别提供了与整数类型和浮点数类型大小限制相关的详细信息
在 limits.h 中:
	#define INT_MAX +32767
	#define INT_MIN -32768
*/

数组是按顺序存储的一系列类型相同的值

字符数组【不以'\0'结尾】与字符串【以'\0'结尾,也是字符数组】有些微区别

char char_array[20];  // 数组声明

3 符号和语句

  • 关键字:while、typedef
  • 运算符:=、-、*、/、%、++、-- 、sizeof
  • 复合语句、自动类型转换和强制类型转换
#define SCALE 2.7;
// 表达式的计算顺序【() 的优先级最高,先计算 () 的部分】
float butter, n = 6.0;
butter = 25.0 + 60.0 * n / SCALE;  // 计算遵循从左到右的顺序
// 先 60.0 * n 得temp1;再 temp1 / SCALE 得temp2;最后 25.0 + temp2 结果赋值给 butter

int a = 1, b = 1, a_post, b_pre;
a_post = a++;  // 后缀:使用a的值之后,递增a
b_pre = ++b;  // 前缀:使用b的值之前,递增b

/* ++ -- 优先级仅次于 () ; ,运算符从右向左执行*/

size_t类型

size_t intsize;  // sizeof返回size_t类型的值【无符号整数类型】,它不是新类型 是(typedef)定义的标准类型
intsize = sizeof(int);  // 可以使用 %zd 显示 size_t类型的值 【也可用 %u、%lu】

/*上面一行代码等价于下面两行*/
int n;
sizeof n;

typedef允许为现有类型创建别名

typedef double real;
real num; // num为double类型

类型的转换【自动转换和强制转换】

/*类型转换的 升级(低精度向高精度) 通常不会有什么问题; 降级会导致麻烦*/
float mice = 1.6 + 1.7;
float mice_2 = (int)1.6 + (int)1.7;  // 强制类型转换

4 循环、条件判断与运算符

  • 循环关键字:forwhiledo......while
do
    statement
while( expression )
// 至少会执行一次循环

for(初始化;判断;更新){ 循环体 }

while(判断){ 循环体 }
  • 条件判断关键字:if、else、switch、continue、break、case、default、goto
  • 关系运算符与算术运算符(前者优先级低于后者,但两者都高于=):
    >、<、>=、<=、!=、==
    &&、||、 ?:
    += -= *= /= %=

使用iso646.h头文件后,可以用 and 代替 &&or 代替 ||not 代替 !

#include <stdio.h>
#include <ctype.h>

void func_char_1() {
	char ch = getchar(); 
	// 该函数不带任何参数,它从输入队列中返回下一个字符 等价于 scanf("%c", &ch);
	// putchar和getchar**只处理字符**,它们比通用的scanf和printf更快

	while (ch != '\n') {  // 输入字符以 换行符 结束
		if (ch == ' ') putchar(ch);
		// putchar()函数打印它的参数 等价于 printf("%c", ch);
		else putchar(ch + 1);
		ch = getchar();  // 获取下一个字符
	}
	putchar(ch);

	// 上面的程序 会将句子中间、结尾的标点也转换。解决此问题 需要使用ctype.h【包含isalpha()的函数原型】
}

void func_cahr_2() {
	char ch;  // 该函数不带任何参数,它从输入队列中返回下一个字符 等价于 scanf("%c", &ch);

	while ((ch = getchar()) != '\n') {
		if (isalpha(ch)) putchar(ch+1); // 如果是字符 显示它的下一个字符
		else putchar(ch);  // 不是则原样显示
	}
	putchar(ch);

	// 此外 tolower(ch)、toupper(ch)会将字符转换成小写、大写字符
	// ctype.h还有很多【字符测试函数】
}

5 输入/输出

  • 输入、输出以及缓冲输入和无缓冲输入的区别

缓冲输入和无缓冲输入

  • 无缓冲输入:回显用户输入的字符后立即重复打印该字符(正在等待的程序可立即使用输入的字符)
  • 缓冲输入:用户在按下[Enter]之前不会重复打印刚输入的字符,用户输入的字符被收集存储在缓冲区(buffer)
  • 如何通过键盘模拟文件结尾条件

现代文本文件不一定有嵌入的Ctrl+Z,如果有一定会将其是为一个文件结尾标记
C语言在 getchar()和scanf()函数检测到文件结尾时都会返回EOF(End of file) ,定义在stdio.h中——#define EOF (-1)
手动输入时,想要结束文件,不能通过输入EOF或者-1的方式,而是:Ctrl+D【Unix】 Ctrl+Z【PC】


C语言处理文件——C程序处理的是(stream)而不是直接处理文件
是一个实际输入或输出映射的理想化数据流。打开文件的过程就是把流与文件相关联。
C把输入和输出设备视为存储设备上的普通文件


stdin流和stdout流分别表示键盘输入和屏幕输出
getchar()、putchar()、printf()、scanf()都是标准I/O包成员,处理这两个流

  • 如何通过重定向把程序和文件相连接(假设prog是可执行程序名 file1、file2是文件名)
    • 把输出重定向至文件: >
      prog >file1
    • 把输入重定向至文件: <
      prog <file2
    • 组合重定向:(把file2作为输入、file1作为输出)
      prog <file2 >file1
      prog >file1 <file1

  • 函数:
     fopen()、getc()、putc、exit、fclose、
     fprintf、fscanf、fgets、fputs、
     rewind、fseek、ftell、fflush、
     fgetpos、fsetpos、feof、ferror、
     ungetc、setvbuf、fread、fwrite
  • 文件模式和二进制模式【C的两者文件模式】、文本和二进制格式、缓冲和无缓冲IO

文件通常是在磁盘或固态硬盘上一段已命名的存储区,C把文件看作是一系列连续的字节,每个字节都能被单独读取
底层I/O【OS提供的基本I/O服务】,标准高级I/O【使用C库的标准包和stdio.h头文件定义,比底层I/O有优势】
C程序会自动打开3个文件:标准输入(standard input)、标准输出(standard output)和标准错误输入(standard error output)

通过下面的C程序了解上述概念:

#include <stdio.h>
#include <stdlib.h>  // 提供exit()函数的原型

void read_file(int argc, char * argv[]) {
    int ch;  // 读取文件是,存储每个字符
    FILE* fp;  // 文件指针
    unsigned long count = 0;
    if (argc != 2)
    {
        printf("Usage: %s filename\n", argv[0]);
        exit(EXIT_FAILURE);  // EXIT_FAILURE【失败结束程序】——宏定义在stdlib.h中
    }
    if ((fp = fopen(argv[1], "r")) == NULL)  // fopen(file_name, 打开方式)
    {
        /* "r"读模式打开
           "w"写模式打开,把现有文件的长度截为0,若文件不存在,则创建一个新文件
           "a"写模式打开,在现有文件末尾添加内容,若文件不存在,则创建一个新文件
           "r+"更新模式打开(读写)
           "w+"更新模式打开,把现有文件的长度截为0,若文件不存在,则创建一个新文件
           "a+"更新模式打开,在现有文件末尾添加内容,若文件不存在,则创建一个新文件
           "rb"、"wb"、"ab"、"ab+"、"a+b"、"wb+"、"w+b" 二进制模式

           C11新增带x的写模式  "wx"、"wbx"、"w+x"、"wb+x"
             - 若以传统的一种写模式打开一个现有文件、fopen会把该文件的长度截为0【丢失了文件内容】
               使用带x的写模式,既是fopen失败,原文件内容也不会被删除
             - 若环境允许,x模式的独占特性使得其他程序或线程无法访问正在被打开的文件

          fopen成功打开文件后会返回 文件指针
        */
        printf("Can not open: %s filename\n", argv[1]);
        exit(EXIT_FAILURE);
    }
    while ((ch = getc(fp))!=EOF)
    {
        /* getc()和putc()函数与getchar()和putchar()函数类似,不同的是要告诉getc() putc()函数使用哪一个文件
        * 
        * ch = getchar(); 表示从标准输入中获取一个字符
        * ch = getc(fp); 表示从fp指定的文件中获取一个字符
        */
        putc(ch, stdout);  // stdin、stderr
        count++;
    }
    fclose(fp);  // 关闭fp指定的文件,必要时刷新缓冲区【成功关闭返回0、否则返回EOF】
    printf("File %s has %lu charactrers\n", argv[1], count);
}

fprintf()fscanf()工作方式与printf()scanf()类似,需要用第一个参数指定待处理的文件

fprintf(stdout, "can't open ... file.\n");
           
while((fscanf(stdin, "%40s", words) == 1) && (words[0] != '#'))  // 标准输入流键入文字,new line的'#'结束输入
    fprintf(fp, "%s\n", words);

rewind(fp);  // 返回文件开始处

fgets(buf, STLEN, fp)fputs(字符串地址buf, fp)

这里的buf是char类型数组的名称,fgets函数读取输入直到第一个换行符的后面,或读到文件结尾、或读取STLEN-1个字符
fputs在打印字符串是不会在其末尾添加换行符

fseek()【返回int类型】和ftell()【返回long类型:文件中当前位置】

fseek()把文件看作是数组,在fopen()打开的文件中直接移动到任意字节处

fseek(fp, 0L【偏移量】, SEEK_END【起始点】); //定位到文件末尾  SEEK_CUR当前位置、SEEK_SET文件开始处
long last = ftell(fp);
fseek(fp, -count, SEEK_END); // count为long类型,从文件末尾出回退
/* 
int ungetc(int c, FILE * fp)函数把C指定的字符放回输入流中(这样下次调用标准输入函数时将读取该字符)

int fflush(File * fp)刷新缓冲区,输出缓冲区中所有未写入数据被发送到fp指定的输出文件。若fp为空指针,所有输出缓冲区都被刷新

int setvbuf(FILE * restrict fp, char * restrict buf, int mode, size_t size) 创建了一个供标准I/O函数替换使用的缓冲区
        fp指向待处理的流,buf指向待使用的存储区【若buf值不为NULL,则必须创建一个缓冲区】
        mode: _IOFBF完全缓冲(缓冲区满时刷新)
               _IOLBF行缓冲(缓冲区满时或写入下一个换行符时)
               _IONBF无缓冲(操作成功返回0.否则返回一个非0值)
*/

fread()fwrite()

之前介绍的标准I/O函数都是面向文本的(用于处理字符和字符串),要在文件中保存数值数据首先想到fprintf()函数

double num = 1. / 3.;
fprintf(fp, "%2.f", num);  //将结果存储为4个字符"0.33"【转化为字符串存储】。改变转换说明将改变存储该值需要的空间,导致存储不同的值  

// 为了保证数据在存储前后一致,如存储数据需要以二进制形式存储,这时需要fread()和fwrite()函数
size_t fwrite(const void * restrict ptr, size_t size, size_t nmemb, FILE * restrict fp)
size_t fread(const void * restrict ptr, size_t size, size_t nmemb, FILE * restrict fp)  //ptr为待读取文件在内存中的地址


/* 标准输入函数返回EOF、读取错误的情况*/
int feof(FILE * fp)int ferror(FILE * fp)
}

6 函数与指针

  • 一元运算符: *、&
  • 函数定义、使用参数和返回值、指针变量作为函数参数 【组织程序的思想就是把函数用作构件】
  • 递归

通过一个简单的C程序了解以上概念:

#include <stdio.h>
#define WIDTH 40

/*1.函数原型  没有使用starbar(void)形式指定该函数确实没有参数,##它将不会检查参数##*/
void starbar();  
void func_pointer(int u, int v);
void func_pointer_2(int* u, int* v);

void up_and_down(int n) {  // 既是函数原型也是函数定义
	printf("Level %d: n location %x\n", n, &n);  // %x(不推荐)
	// %p:000000CF06EFFB00(win64)、001DFDF8(win32)    %x:85affab0(win64)、12ff79c(win32)
	// %p来输出指针(无符号整数)的值、输出地址符  使用64位编译器会打印8个字节(64位),左边空缺会补零
	// 如果编译器不识别%p 用%u或者%lu代替
	if (n < 4)  // 递归函数一定要定义出口条件,否则会造成系统栈溢出
	{
		up_and_down(n + 1);  // 递归调用
	}
}

int avoid_main_8(){
	int x = 5, y = 8;

	printf("below is starbar.\n");
	starbar();  /*3.函数调用*/

	//int imax(int, int);  /* 还可以在主调函数中声明函数,该函数接受两个int型的参数*/

	up_and_down(1);

	printf("in main:originally x = %d, y = %d\n", x, y);
	func_pointer(x, y);
	printf("in main:after swap x = %d, y = %d\n", x, y);

	printf("use pointer in main:originally x = %d, y = %d\n", x, y);
	func_pointer_2(&x, &y);  // 注意:这里传的参数是地址
	printf("use pointer in main:after swap x = %d, y = %d\n", x, y);

	return 0;
}

void starbar(){  /*2.函数定义*/
	int count;
	for (count = 0; count < WIDTH; count++) {
		putchar('*');
	}
	putchar('\n');
}

指针初识(交换两数的位置):

指针是C语言中最重要(复杂)的概念之一,用于储存变量的地址。
一元运算符 & 给出变量储存的地址

void func_pointer(int u, int v) {
	int temp;
	printf("in func:originally u = %d, v = %d\n", u, v);
	temp = u;
	u = v;
	v = temp;
	printf("in func:after swap u = %d, v = %d\n", u, v);

	// 这里最后显示的结果是:在方法内两数成功交换,但是在main方法调用后,两数却没有交换
	// 问题出在把结果传回mian()时,函数func_pointer使用的并不是main中的变量(x,y),因此交换u,v对x,y的值没有影响
}

void func_pointer_2(int *u, int *v) {
	/* 对上述交换算法的指针形式改进,利用指针在函数间通信
	  使用间接运算符'*'  

	  int *pi;  // pi是指向int类型变量的指针
	  假设pi指向int型变量val,则 pi == &val;
	  int temp = *pi;  // 找出pi指向的值赋给temp
	  */

	int temp;
	temp = *u;
	*u = *v;  // 区别在于,这里将指定地址的值进行交换,不会出现上一个函数的交换不同变量的问题
	*v = temp;
}

二维数组(探讨此时指针与数组的关系)

假设arr是二维数组, arr[0]是该数组首元素arr[0][0]的地址,所以 *(arr[0]) == arr[0][0]
类似地:*arr代表一维数组首元素arr[0],代表二维数组首元素的地址&arr[0][0]

static int arr[6] = {[5] = 212 };  // 静态变量。C99  设置 arr[5]

**arr == *&arr[0][0]

指针数组与数组指针

int (* pi)[2];  // pi指向一个内含两个int类型值的数组,pi为指向一个数组的指针  【数组指针】
// pi[i]代表一个由2个整数构成的元素,pi[i][j]代表一个整数

int * pz[2];  // pz是一个内含两个指针元素的数组,每个元素都指向int的指针  【指针数组】

①注意(指针变量的赋值):

int n = 5;
double x;
int * p1 = &n;
double* p2 = &x;
x = n;  // 隐式类型转换
//p1 = p2;  语句会在编译时错误

②注意(const用法)

C const 和 C++ const  (用法相似)
  C++允许在声明数组大小时使用const整数,C不允许
  C++的指针赋值检查更严格
 const int y;
 const int * p2 = &y;
 int * p1;
 p1 = p2; // C++不允许【把const指针赋值给非const指针】,C会给出警告,但是通过p1更改y的值是未定义的

③扩展知识

变长数组(VLA)
int arr[][4];  // 可以是arr[5][4]、arr[100][4]、arr[20][4]... 即可以使用变量 改变数组的维度

// 扩展:复合字面量(匿名变量)

7 字符串相关函数

gets()、gets_s()、fgets()、puts()、puts()、fputs()

strlen()、strcat(str1, str2)【拼接两个字符串】、strncat(str1, str2, n)【n指定str2拼接str1的长度】

strcmp(str1, str2)【比较两个储存在不同数组大小的字符串,大小写要一致,返回比较字符串结果的ASCII码排序,相等为0】

strncmp()【适合查找以“moon”开头的字符串,限定只查找这4个字符】

strcpy()、strncpy()

sprintf()【不同于上述方法(声明在string.h中),将数据写入字符串,可以将多个元素合成一个字符串。第一个参数是目标字符串的地址,其余参数和printf()相同】

以下面的C程序为例:

#include <stdio.h>

#define MSG "I'm a symbolic string constant."

int func_string_1(void) {
    char words[81] = "I am a string in an array.";
    const char* pt1 = "Pointer is pointing at me.";
    puts(MSG);  // puts函数只显示字符串,而且自动在显示的字符串末尾加上换行符
    puts(words);
    puts(pt1);
    words[8] = 'p';
    puts(words);
    return 0;

    // 读取字符串时,scanf()函数和转换说明%s只能读取一个单词,gets()函数用于处理这种情况【读取整行输出,直到遇到换行符】
    // 但是由于:char words[81]; 是定长的,gets()唯一的参数是words【字符串数组名】,它无法检查数组是否装得下输入行
    // 若输入字符串过长,会导致缓冲区溢出(buffer overflow)-- C99建议不要使用。C11直接废除
}

gets()函数的替代:

  • 过去常用fgets()代替gets()fgets()通过第二个参数限制读入的字符来解决溢出的问题,该函数专门用来设计处理用于文件输入
    • fgets()的第二个参数指明了读入字符的最大数量,若参数为n,则读入n-1个字符,或者读到第一个换行符为止
    • fgets()读到第一个换行符会把它储存在字符串中【gets()会丢弃换行符】
    • fgets()的第三个参数指明要读入的文件。如果读入从键盘输入的数据,则以stdin作为参数
    • 同样的。fputs(words,stdout)能显示输出手动输入的字符串,返回指向char的指针。【但是如果读到文件结尾,它将返回空指针

(空指针有一个地址值,该值不会与任何数据的有效地址对应。空字符【'\0'为字符占1字节】和空指针【地址,4字节】都可以理解为 0)

  • 【C11新增的gets_s()也可代替】
    • gets_s(words, STR_LEN)只从标准输入中读取数据,所以不需要第三个参数【文件】
    • gets_s()读到换行符会丢弃它
    • gets_s()读到最大字符数都没有读到换行符,首先会把目标数组中的首字符设置为空字符,读取并丢弃随后的输入直至读到换行符或者文件结尾,然后返回空指针,接着调用依赖实现的“处理函数”,可能会中止或者退出程序

8 存储、链接

因为C是面向过程编程的语言,它的对象指的是存储值占用的内存
指定对象的表达式称为左值

int entity = 3; // 该声明创建了一个名为entity的标识符,用来指定特定对象的内容

int a = 0; // 文件作用域(外部文件可用)
static int b = 1; // 文件(全局)作用域  static声明的是它链接属性,而非存储期
// 上面的文件作用域变量都具有静态存储期

  • 关键字:auto、extern、static、register、const、volatile、restricted、_Thread_local、_Atomic
  • 函数:rand()【伪随机数,在其取值范围内均匀分布】、srand()、time()、malloc()、calloc()、free()
  • 变量的作用域和生命周期

存储期

  • 静态存储期:(程序执行期间一直存在)
  • 线程存储期:用于并发程序设计,从被声明到线程结束一直存在。以_Thread_local声明一个对象时,每个线程都获得该变量的私有备份
  • 自动存储期:块作用域的变量,程序进入定义这些变量的块时为其分配内存、退出时释放内存
  • 动态分配存储期

可以显式地使用 auto表示自动变量【自动存储周期、块作用域、无链接】

寄存器变量【数据类型有限,比如可能没有足够大的空间存储double类型的值】使用register声明,比一般的变量(存储在内存中)更快【自动存储周期、块作用域、无链接】

块作用域的静态变量:块中使用static声明,静态的意思是该变量在内存中原地不动,并不是值不变

多文件【程序由多个单独的源代码文件组成】
这些文件可能要共享一个外部变量。C通过在一个文件进行定义式声明,然后在其他文件中进行引用式声明实现共享。
除了一个定义式声明(才能初始化变量)之外,其它声明都要使用extern关键字

time()返回值的类型为time_t,接受的参数是time_t类型对象地址,时间存储与在传入的地址上。

5种存储类别

  • 自动 【auto可省略】——自动存储期、块作用域、无链接
  • 寄存器 【register】——自动存储期、块作用域、无链接,无法获取其地址
  • 静态、无链接 【块中 static】——静态存储期、块作用域、无链接
  • 静态、外部链接 【函数外部 无static】——静态存储期、文件(外部)作用域、外部链接
  • 静态、内部链接 【函数外部 static】——静态存储期、文件(内部)作用域、内部链接

通过下面的C程序理解:

#include <stdio.h>
#include <stdlib.h>  // 提供库函数rand()、malloc()、free()的原型
//#include <stdatomic.h>

int roll_count = 0;
static int rollem(int sizes) {  //该文件私有函数
    int roll;
    roll = rand() % sizes + 1; // rand()取值范围0~RAND_MAX之间,实现掷骰子得到1~6的结果需要该行代码
    ++roll_count;  // 函数的调用次数

    return roll;
}

malloc()函数

malloc()函数在程序运行时分配更多的内存,接受一个参数:所需的内存字节
malloc()函数会找到合适的空闲内存块,这样的内存是匿名的【分配内存但是不为其赋名】,返回动态分配内存块的首字节地址。它的返回类型通常被定义为指向char的指针。


注意:malloc()函数可用于返回指向数组的指针、指向结构的指针等,所以它的返回值会被强制转换成匹配的类型,malloc()分配内存失败将返回空指针


从ANSI C开始,新类型:指向void的指针——“通用指针”

double * ptd;
ptd = (double *)malloc(30 * sizeof(double));  // 一定要手动释放
//... 代码
free(ptd);
--------------------
long * newmem;
newmem = (long *)calloc(100, sizeof(long));  // calloc接受两个无符号整数作为参数
...
free(newmem);

malloc()与变长数组

void func_vla() {  // VLA 变长数组
    //int n;
    //scanf("%d", &n);
    //int arr[n];// 变长数组【自动存储类型】 不用free() 函数结束后自动释放

    // malloc()创建二维数组
    int(*p2)[6];
    //int(*p3)[m];  // 需要编译器支持变长数组
    //p2 = (int (*) [6])malloc(n * 6 * sizeof(int));
    //p3 = (int (*) [m])malloc(n * m * sizeof(int));

    int arr[2][2] = { {1,2}, {3, 4} };
    int(*pi)[2];  // 它相当于一个二维数组的用法,只是它是一个n行2列的数组
    // 等价于 int * pi = (int *)malloc(2 * sizeof(int));

    pi = arr;

    printf("%d\n", *pi[1]);  // 相当于 arr[1][0]
    printf("%d\n", pi[0][1]);  // 相当于 arr[0][1]
    printf("%d\n", (* pi + 2)[1]);  // 相当于 arr[1][1]  对pi相加——某一行。中括号——某一列
}

新特性

C90新增两个属性:恒常性(constancy)和易变性(volatility)
C99新增第三个限定符:restrict,用于提高编译器优化
C11新增第四个限定符:_Atomic(可选支持项)以及一个可选库(stdatomic.h——并发程序设计)

const const const int n = 6;  //C99新属性  声明中多次使用同一个限定符,多余的会被忽略

/*volatile限定符,代理(而非变量所在的程序)可以改变该变量的值
  通常被用于硬件地址以及其他程序或同时运行的线程中共享数据*/
  
volatile int locl;  // locl是一个易变的位置
volatile int* ploc; // ploc是一个指向易变的位置的指针

// 使用示例:用const把硬件时钟设置为程序不能更改,但可以通过代理改变
volatile const int loc;
const volatile int* ploc;

/*restrict类型限定符允许编译器优化部分代码以更好地支持计算 
  它只能用于指针,表明指针是访问数据对象的唯一且初识的方式
  */
void func_restrict() {
    // void ofmouth(int * const a1, int * restrict a2, int n);// old style
    // void ofmouth(int a1[const], int a2[restrict], int n);// C99

    int ar[10];
    //int * restrict res = (int*)malloc(10 * sizeof(int));  //C99
    int * par = ar;

    for (int i = 0; i < 10; i++) {
        par[i] += 5;
        //restar[i] += 5;
        ar[i] *= 2;
        par[i] += 3;
        //restar[i] += 3;
    }
    /*
    因为restar是访问分配的内存的唯一且初始的方式,那么 编译器 可以将上述对restar的操作进行优化:
  restar[n]+=8;
  而par并不是访问 数组 ar的唯一方式,因此并不能进行下面的优化:
  par[n]+=8;
  因为在par[n]+=3前,ar[n]*=2进行了改变。使用了 关键字 restrict,编译器就可以放心地进行优化了。


  C库中有两个函数可以从一个位置把字节复制到另一个位置。在C99标准下,它们的原型如下:
  void * memcpy(void * restrict s1, const void * restrict s2, size_t n);
  void * memove(void * s1, const void * s2, size_t n);

  这两个函数均从s2指向的位置复制n字节数据到s1指向的位置,且均返回s1的值。两者之间的差别由关键字restrict造成,
    即memcpy()可以假定两个内存区域没有重叠。memmove()函数则不做这个假定
    因此,复制过程类似于首先将所有字节复制到一个临时缓冲区,然后再复制到最终目的地。
    如果两个区域存在重叠时使用memcpy()会怎样?其行为是不可预知的,既可以正常工作,也可能失败。
    在不应该使用memcpy()时,编译器不会禁止使用memcpy()。因此,使用memcpy()时,必须确保没有重叠区域。

 关键字restrict有两个读者。一个是编译器,它告诉编译器可以自由地做一些有关优化的假定。另一个读者是用户,他告诉用户仅使用满足restrict要求的参数。
    */
}

void func_atomic() {
    int hogs;  // 普通声明
    hogs = 12;  // 普通赋值

    //可以替换成
    //_Atomic int hogs;  // hogs为一个原子类型的变量  C11
    //atomic_store(&hogs, 12);  // stdatomic.h中的宏
}

9 结构体

  • 关键字:struct、union、typedef
  • 运算符:.->
  • C结构、创建结构模板和结构变量
  • 访问结构程序,编写处理结构的函数
  • 联合体指向指针的函数

通过下面的程序了解:

#include <stdio.h>
#include <string.h>
#define MAXTIT 41  // 书名最大长度
#define MAXAUTH 31  // 作者名字最大长度

typedef struct book {  
// book结构--传统方式打印一份图书目录【包含每本书的详细信息:书名、作者、出版社、页数、价格等】
//需要创建多个不同类型的数组,十分繁琐,使用一些特定操作【如按出版日期排序】也比较麻烦
	char title[MAXTIT];
	char author[MAXAUTH];
	float value;
}library;

void book_struct_use() {
	//struct book library; // 等价于不用typedef定义struct book的别名,在此处声明
	printf("please enter the book title:\n");
	/*gets(library.title, MAXTIT);*/

	struct book* b; // 指向结构的指针
	// 可以使用 b->value的方式告诉编译器指针指向何处

	/*
	带参函数 传入结构体成员【仅仅使用结构提成员的值,不改变它】
	        传入结构体成员地址【改变它的值】
			传入结构【结构体中的成员需要初始化】
	*/
}

union是一种数据类型,它能在同一个内存空间中存储不同的数据类【不是同时存储】
典型用法:设计一种表存储既无规律、事先也不知道顺序的混合类型

union hold {  // 声明的联合hold只能存储一个int或一个double或是一个char类型的值
	int digit;
	double bigfl;
	char letter;
};

union hold fit;  // 编译器分配给他足够的空间以便它能存储union声明中占用最大字节的类型【double--8byte】
union hold save[10]; // 每个元素都是分配8字节

/*用法
fit.digit = 23; //23存储在fit 占2字节
fit.bigfl = 2.0; //清除23,存储2.0 占8字节
fit.letter = 'h'; //清除2.0,存储'h' 占1字节
*/

枚举类型enum,声明符号名称来表示整型变量【实际上enum常量是int类型,目的是提高程序的可读性】

enum spectrum { red, orange, yellow, green, blue, violet};  // 枚举了spectrum可能的值  枚举值输出的话默认为0、1、...   也可以自定义为其赋值
enum spectrum color;  // 声明

if(color==yellow)...  

for(color=red; color<=violet; color++)...  
// 【C++的枚举变量不支持++运算符】

函数和指针

void ToUpper(char *);

void (*pf)(char *);  // pf是一个指向函数的指针  可以用pf指向ToUpper函数的地址:pf = ToUpper;

void *pf(char *);//pf是一个返回字符指针的函数

10 位操作、预处理库

  • 运算符:~【按位取反】、&【按位与:常用于掩码】、|【按位或】、^【按位异或】、 <<【左移 若 二进制数<<2,则高位丢失,低位补零】、>>【右移,有符号数和无符号数有区别】、 &=、|=、^=、>>=、<<=
  • 关键字:_Alignas【给出一个类型的对齐要求】、_Alignof
  • 预处理指令:#define【明示常量】、#include
    #undef用于取消已定义的#define指令
    #line 用于重置行和文件信息
    #error 给出错误信息
    #pragma 用于向编译器发出指令
    #ifdef、#else、#endif、#ifndef、#if、#elif 用于指定什么情况下编写哪些代码
#ifdef MAVIS
  #include "horse.h"
  #define STABLES 5
#else
  #include "cow.h"
  #define STABLE 10
#endif
  • 关键字:_Generic、_Noreturn、_Static_assert
  • 泛型选择表达式:
_Generic(x, int: 0, float: 1, double: 2, default: 3)  // C11
  • 函数说明符:_Noreturn,调用完成后函数不返回主调函数 【C11】

  • 函数/宏:sqrt()、atan()、atan2()、exit()、atexit()【通过退出时注册被调用的函数提供这种功能,介绍一个函数指针作为参数】

    • 宏(macro)代表值——类对象宏;代表函数——类函数宏 【代码替换】,从宏替换成最终代码的过程称为宏展开(macro expansion)
    • 示例: #define MEAN(X,Y) (((X)+(Y))/2) 前者MEAN(X,Y)为宏,X、Y为宏参数,后者为替换体
    • 预处理粘合剂:##运算符 如:#define XNAME(n) x ## n 宏XNAME(4)将展开为x4
    • 变参宏:...__VA_ARGS__ 【比如printf()介绍数量可变的参数,可以通过预处理和宏定义的方式:#define PR(...) printf(__VA_ARGS__) 后面调用PR("hello");
  • 内联函数——C99提供,起因是由于函数调用过程:建立调用、传递参数、跳转到函数代码并返回,使用宏是代码内联可以避免这样的开销

inline static void eatline(){//内联函数定义
           ...
}
int main(){
  ...
   eatline()//调用
   ...
}

C程序:

#include <stdio.h>

// 有符号整数和无符号整数的二进制表示   原码、反码和补码

void why_me();
void pre_macro() {  // C语言的一些预定义宏
    printf("The file is %s.\n", __FILE__);
    printf("The date is %s.\n", __DATE__);
    printf("The time is %s.\n", __TIME__);
    //printf("The version is %ld.\n", __STDC_VERSION__);  // 支持C99:199901L、支持C11:201112L
    printf("This is line %d.\n", __LINE__);  // 当前源代码文件中行号的整型常量
    printf("This function is %s.\n", __func__);  // 预定义标识符,展开为一个代表函数名的字符串  C99
    why_me();
}

void why_me() {
    printf("This function is %s.\n", __func__);
    printf("This is line %d.\n", __LINE__);
}


/*
数学库:math.h
泛型类型库:tgmath.h
通用工具库:stdlib.h  如rand()、srand()、malloc()、free()
断言库:assert.h  assert()宏,用于辅助调试程序   C11新增:_Static_assert,在**编译时**检测assert()表达式
字符串库:string.h  memcpy()、memmove()
可变参数:stdarg.h  宏:vastart()、va_arg()、va_copy()、va_end()
*/
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值