C 语言学习笔记

C 语言笔记

操作符

  • ‘- -’ ,‘++’

    后置:先使用再操作
    前置:先使用后赋值

  • (类型符) 强制类型转化
    在这里插入图片描述

  • 条件操作符 exp1 ? exp2 : exp3

    exp1 为真, exp2 执行,为假 exp3 执行。

  • 下标引用
    [ ] ----------- (数组)下标引用操作符
    ( ) ----------- 函数调用操作符
    . ------------- 结构体.成员
    -> ----------- 指针-> 成员

常见关键字

  • auto
    自动变量 == 局部变量,一般省略。
    auto int a = 10;
  • register
    寄存器变量,高速访问。
  • typedef
    类型定义–重定义
  • extern
    在调用其他.c文件中的变量或者函数时,需要在使用前用extern申明。
  • static
    当修饰局部变量时,局部变量的生命周期变长,下次进入局部作用域时变量值保留(跳过变量初始化赋值);
    当修饰全局变量时,改变作用域。静态的全局变量只能在自己所在的源文件内部使用(extern 使用无效);
    当修饰函数时,改变函数的链接属性,外部链接变为内部链接属性(extern 使用无效)。
  • #define
    可定义标识符常量;
    #define PI 3.1415926
    可定义函数(宏);
    #define MAX(X,Y) (X>Y?X:Y)
  • const
    const 修饰的变量无法修改;但是可以通过指针修改。
const int num = 10;
int* p = #
*p = 20;

const 修饰指针时有两种情况。

const int num = 10;
const int* p = #
// 错误!!!!
*p = 20; // const 放在指针变量的*左边时,修饰*p,不能改变*p的值

const int num = 10;
int num_2 = 20;
int* const p = #
// 错误!!!!
p = &num_2; // const 放在指针变量的*右边时,修饰p,不能改变p的值

但是修饰指针时情况不一样。


数组

数组名

  • 数组名是数组首元素的地址。(有两个例外)
    1.sizeof(数组名),计算整个数组的大小。
    2.&数组名,取出的是数组的地址。

指针

指针数组和数组指针

int* p[10];
int(*p)[10];

[ ] 比*优先级更高。第一个是数组,存放10个指向int的指针;第二个是指针,指向含有10个int型的数组。

函数指针

int Add(int x, int y)
{
	return x + y;
}
int main()
{
	int (*p)(int, int) = &Add;
	int (*p)(int, int) = Add;
	return 0;
}

*p 取的是函数 Add 的地址,() 表示函数。

void(*signal(int, void(*)(int)) )(int);

typedef void(*fun)(int);
fun signal(int, fun);

signal是一个函数申明;
signal函数参数有两个,第一个是int,第二个是函数指针,该函数指向的函数参数是int,返回类型是void;
signal函数的返回类型也是一个函数指针,该函数指针指向的函数参数是int,返回值是void。

空指针

  • void * p;
    该指针可以接受任意类型的地址;但是不能进行解引用和±

结构体

结构体内存对齐

  1. 第一个成员放在首地址处;
  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
    对齐数 = 编译器默认的一个对齐数与该成员大小的较小值。
    vs 中默认的值为8
  3. 结构体的大小为最大对齐数的整数倍。
  4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

对齐原因
6. 平台原因(移植原因):不是所有硬件平台都能访问任意地址上的任意数据;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出异常。
7. 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

总体来说,内存对齐是以空间换时间

设置默认对齐数

#include <stdio.h>
#pragma pack(4) //设置默认对齐数为4
struct S
{
	char c1;
	double d1;
};
#pragma pack() //取消默认对齐数

位段

声明

  1. 位段成员必须是整型量。
  2. 位段成员名后面有一个冒号和数字。
  3. 冒号后的数字代表所占比特数(二进制位)。
struct A
{
	int _a : 2;
	int _b : 5;
	int _c : 10;
};

内存分配

  1. 位段的成员可以是整型量或者char(属于整型家族)。
  2. 位段的空间上是按照需要以4个字节或者一个字节的方式来开辟的。
  3. 位段涉及很多不确定因素,是不跨平台的,可移植程序应避免使用位段。

枚举

把可能的值一一列举,只能用里面的值进行结构体赋值。

enum Sex
{
	MALE,   // 默认0,可以更改 MALE = 2,
    FEMALE, // 默认1
    SECRET, // 默认2
};

优点

  1. 增加代码的可读性和可维护性
  2. 和 #define 定义的标识符比较,枚举有类型检查,更加严谨
  3. 防止命名污染,便于调试
  4. 使用方便,一次可以定义多个常量

联合体(共用体)

定义

联合也是一种特殊的自定义类型,这种类型定义的变量也包含一系列的成员,特征是这些成员共用同一块空间。

union un
{
	char c;
	int i;
};

特点

联合体的大小,至少是最大成员的大小。
联合体的成员不能同时修改。


动态内存管理

malloc 和 free

  • void* malloc ( size_t size);
  • void free (void* memblock);
#include <stdlib.h>
int* p = (int*)malloc(10 * sizeof(int));
free(p);    //p指针的地址没改变,依旧可以修改内容。
p = NULL;  // 改变地址。 

calloc

  • void* calloc(size_t num, size_t size);
    分配空间并置零。
int* cp = (int*)calloc(10, sizeof(int)); // 开辟10个整型空间并置零
free(p);
p = NULL;

realloc

  • void* realloc(void* ptr, size_t size);
    调整动态开辟空间的大小
int* p = (int*)malloc(10 * sizeof(int));
int* p1 = (int*)realloc(p, 20 * sizeof(int)); // 调整后新大小为20*4个字节
free(p1);
p1 = NULL;

p 和 p1指向的空间可能不一样。如果内存可以追加,则一样;若不能追加会重新开辟空间并将原数据拷贝到新空间,释放旧的内存空间,两者则不一样。

常见动态内存错误

  1. 对空指针解引用操作。malloc开辟失败,返回指针指向NULL。
  2. 对动态开辟内存的越界访问。程序卡死。
  3. 对非动态开辟内存的free。程序卡死。
int a = 10;
int* p = &a;
free(p);
  1. 使用free释放动态开辟内存的一部分。free释放要从起始位置开始。
int* p = (int*)calloc(10, sizeof(int));
p++;
free(p);
  1. 对同一块开辟的内存多次释放。可通过free后置空指针避免重复释放出错。
  2. 动态开辟内存忘记释放(内存泄露)

柔性数组

结构中最后一个元素允许是未知大小的数组,这就叫做柔性数组成员。

struct st   
{
	int a;
	int a_1[];
};
printf("%d\n", sizeof(struct st)); // 4个字节
struct st* s = (struct st*)malloc(sizeof(struct st) + 10 * sizeof(int)); // 为a_1开辟了10个整型变量空间。

与结构体内有指针成员,指针动态开辟空间相比,有以下好处:

  1. 方便内存释放(仅需一次);
  2. 有利于访问速度,减少内存碎片。

内存分配

在这里插入图片描述

  1. 栈区(stack):存储函数的局部变量(包括main函数),函数执行结束后自动释放。
  2. 堆区(heap):存储动态内存。一般由程序员分配释放,若不释放,程序结束后收回。
  3. 数据段(静态区 static):存放全局变量、静态数据。程序结束后由系统释放。
  4. 代码段:存放函数体(类成员函数和全局函数)的二进制代码。

文件操作

文件指针

每一个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息。这些信息是保存在一个结构体变量中的,该结构体类型是有系统声明的,取名 FILE。一般都是通过一个FILE 指针来维护这个FILE结构的变量,使用更方便。

文件的打开和关闭

  • FILE* fopen ( const char* filename, const char* mode);
打开方式含义如果指定文件不存在
“r” (只读)输入数据,打开已存在文本文件出错
“w” (只写)输出数据,打开一个文本文件创建新文件
“a” (追加)向文本文件尾添加数据出错
“rb” (只读)输入数据,打开二进制文件出错
“wb” (只写)输出数据,打开二进制文件创建新文件
“ab” (追加)向二进制文件尾添加数据出错
“r+” (读写)读写,打开文本文件出错
“w+” (读写)读写,建一个文本文件创建新文件
“rb+” (读写)读写,打开二进制文件出错
“wb+” (读写)读写,建一个二进制文件创建新文件

如果返回空指针,则打开失败。

  • fclose ( FILE* stream);

文件的顺序读写

功能函数名适用于
字符输入函数fgetc所有输入流
字符输出函数fputc所有输出流
文本行输入函数fgets所有输入流
文本行输出函数fputs所有输出流
格式化输入函数fscanf所有输入流
格式化输出函数fprintf所有输出流
二进制输入fread文件
二进制输出fwrite文件

文件随机读取

  • int fseek ( FILE * stream, long int offset, int origin );
    从当前位置开始,从偏移量 offset 开始读取。origin可以有:
    在这里插入图片描述

  • long int ftell ( FILE * stream );
    返回值是偏移量。

  • void rewind ( FILE * stream );
    将文件指针置于起始位置。

  • void perror ( const char * str );
    打印str和错误信息(比strerror(errno)方便)。

  • int feof ( FILE * stream );
    判断是否到文件末尾。若到末尾返回非0值;若没到返回0。

  • int ferror ( FILE * stream );
    判断是否读取错误。


程序的环境和预处理

程序的翻译环境和执行环境

翻译环境

在这个环境中源代码被转换为可执行的机器指令(二进制指令)。包括编译链接
在这里插入图片描述

  • 组成一个程序的每个源文件通过编译过程分别转换成目标代码(object code)。
  • 每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序。
  • 链接器同时也会引入标准C函数库中任何被该程序所使用到的函数,而且它可以搜索程序员的个人程序库,将其需要的函数也链接到程序中。

编译可细分为:预处理编译汇编

  • 预处理
    1.完成头文件包含,#include。
    2.使用空格替换注释。
    3.完成预处理指令,#define。

总的来说,对源码进行文本操作,生成 .i 文件。

  • 编译
    把 C 代码翻译成汇编代码(语法分析,词法分析,语义分析,符号汇总)。

对 .i 文件编译,生成 .s 文件。

  • 汇编
    把汇编代码,转化成二进制指令。

对.s 文件进行汇编,生成.obj 文件。

链接

  1. 合并段表。
  2. 符号表的合并和重定位。

将.obj 文件链接到一起,生成.exe 文件。

运行环境

  1. 程序必须载入内存中。在有操作系统的环境中,一般由这个操作系统完成。
  2. 程序的执行开始。调用main 函数。
  3. 开始执行程序。使用运行堆栈(stack),和静态内存(static)来存储变量。
  4. 终止程序。正常终止main 函数,也有可能意外终止。

预处理详解

预定义符号
符号作用
__ FILE __代码目前所在的绝对路径
__ LINE __代码目前所在的行号
__ DATA __当前日期
__ TIME __当前时间
__ FUNCTION __当前函数名
#define 定义标识符

#开头的都是预处理指令:#include,#program,#if …。在标识符处有介绍。

# — 将参数名插入到字符串中
#define PRINT(X) printf("the value of "#X" is %d\n",X)
调用PRINT 时,#X处会输出传入的变量名。

## — 将两边的符合合成一个符号
#define CAT(X,Y) X##Y

int today1 = 2019;
printf("%d\n", CAT(today, 1)); \\ 输出2019

宏参数可以不限数据类型
#define MAX(X,Y) X>Y? X:Y
调用时可以传入整型量或者浮点型量。

宏与函数相比较
Pros:
1.在执行简单运算上,宏比函数在程序的规模和速度上更胜一筹。
2.宏是类型无关的,可适用于多种类型数据。

Cons:
1.使用宏时会直接替换,若宏很长,会大幅度增加代码长度。
2.宏无法调试。
3.宏由于类型无关,也不够严谨。
4.宏可能会带来运算符优先级的问题,导致程序出现错误。

#undef — 移除一个宏定义

#define CAT(X,Y) X##Y
#undef CAT
条件编译

在编译程序时,可以选择将一组语句有条件的编译。
常见的条件编译指令:

  • 直接条件编译
    if 常量表达式
    // …
    #endif

  • 多分支的条件编译
    #if 常量表达式
    // …
    #elif 常量表达式
    // …
    #else
    // …
    #endif

  • 判断是否被定义
    #ifdef DEBUG
    // …
    #endif

    #ifndef DEBUG
    // …
    #endif

文件包含

#include 指令可以使另外一个文件被编译。替换的方式很简单:预处理器先删除这条指令,并用包含文件的内容替换。如果一个源文件被包含10次,那就实际被编译10次。

  • 本地文件包含
    #include “file.h”。查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件,如果找不到就提示编译错误。

  • 库文件包含
    #include <stdio.h>。查找头文件直接去标准路径下查找,如果找不到就提示编译错误。可以用“ ”的形式包含库文件,不过查找效率低,且不容易区分两者区别。

  • 嵌套包含
    如果一个文件被包含多次,会造成编译多次。一下方式避免:

    1. #ifndef __TEST_H __
      #define __TEST_H __
      / / …
      #endif
    2. #pragma once
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值