C语言笔记

推荐资源

常量与变量

常量

C语言中的常量分为以下几种

  • 字面常量
  • const修饰的常变量
  • #define定义的标识符常量
  • enum枚举常量
#include <stdio.h>
enum Sex {
    MALE,
    FEMALE,
    SECRET
    };

int main() {
    //字面常量演示
    3.14; // 字面常量
    10000; //字面常量

    //const 修饰的常变量
    const float pai = 3.14f; //此处pai是const修饰的常变量
    //pai = 5.14; //是不能够直接修改的

    // #define的标识符常量
    #define MAX 100
    printf("max = %d\n", MAX);

    //枚举常量演示
    printf("%d\n", MALE);
    printf("%d\n", FEMALE);
    printf("%d\n", SECRET);

    return 0;
}

操作符

算数操作符

对于 / 操作符, 如果两个操作数都为整数,执行整数除法,而只要有浮点数执行的就是浮点数除法

移位操作符

  • 移位操作符的操作只能是整数(浮点数在内存中遵从IEEE754标准,与整数不同)
  • 对于移位操作符,不要移动负数位,这个是标准未定义的

sizeof的用法

sizeof是操作符,不是函数
sizeof用于数组时,可用于计算数组元素个数

#include <stdio.h>
int main(){
    int arr[] = {1,2,3,4,5,6,7,8};
    int elem_num = sizeof(arr) / sizeof(arr[0]);
    printf("%d\n", elem_num);
    
    return 0;
}
//不同情境下,sizeof的值不同,注意区分场景
void test1(int arr[]) {
	printf("%d\n", sizeof(arr));  //打印首元素地址的大小
}
void test2(char ch[]) {
	printf("%d\n", sizeof(ch));
}
int main() {
	int arr[10] = { 0 };
	char ch[10] = { 0 };
	printf("%d\n", sizeof(arr));  //打印整个数组大小
	printf("%d\n", sizeof(ch));
	test1(arr);
	test2(ch);

	return 0;
}

逗号表达式

逗号表达式中各表达式的运算顺序为由左到右,整个逗号表达式的最终结果为最后一个(最右侧那个表达式的结果)

逻辑运算

逻辑运算具有短路特性,即:
A && B ,当A为0时,不再计算B,直接判为0;
A || B,当A不为0时,不再计算B,直接判为非0;

//短路逻辑
int main() {
	int i = 0, a = 0, b = 2, c = 3, d = 4;
	i = a++ && ++b && d++;
	printf("a = %d\nb = %d\nc = %d\nd = %d\n", a, b, c, d);
	printf("------------------------------------\n");
	i = 0, a = 0, b = 2, c = 3, d = 4;
	i = a++ || ++b || d++;
	printf("a = %d\nb = %d\nc = %d\nd = %d\n", a, b, c, d);

	return 0;
}

位操作符

位操作符的操作数必须是整数

//不创建临时变量(第三个变量),实现两个数的交换
int main() {
	int a = 3;
	int b = 5;

	printf("交换前:a = %d, b = %d\n", a, b);
	a = a + b;
	b = a - b;
	a = a - b;
	printf("交换后:a = %d, b = %d\n", a, b);
	return 0;
}

int main() {
	int a = 3;
	int b = 5;

	printf("交换前:a = %d, b = %d\n", a, b);
	a = a ^ b;
	b = a ^ b;
	a = a ^ b;
	printf("交换后:a = %d, b = %d\n", a, b);

	return 0;
}

赋值操作符

赋值操作符可以连续使用

问题表达式

由于各操作符间结合性以及优先级特点,可能导致出现一些问题表达式

//表达式1
a*b + c*d + e*f;
/*
该表达式只能确保乘法先于加法执行,但对于同等优先级的乘法,并没有规定执行顺序
若a, b, c等为各种操作,则无法预测的执行顺序可能会导致出现bug
*/

//表达式2
c + --c
/*
同上,操作符的优先级只能确保自减的操作在加法前发生,但不能确保c的执行与--c的顺序
*/

关键字

typedef

typedef可以理解为类型重命名

static

在C语言中,static是用来修饰变量和函数的
全局变量和函数都是具有外部链接属性的,static修饰全部变量的时候,这个全局变量的外部链接属性就变成了内部链接属性,其他源文件(.c)就不能再使用到这个全局变量了
static修饰局部变量的时候,局部变量出了作用域是不销毁的。
本质上,static修饰局部变量时,改变了变量的存储位置(存储在静态区,而非存储变量的栈区),这影响了变量的生命周期,使得该局部变量的生命变得与程序的生命周期一样长

switch

在switch语句中,没办法直接实现分支,必须搭配break语句使用可以实现真正的分支,break语句的实际效果是把语句列表划分为不同的分支语句;
如果在进入一个case后,后续都没有break语句,则后续的case语句都会被执行;

建议:在最后一个case语句的后面加上一条break语句(可以避免出现在以前的最后一个case语句后面忘了添加break语句)

建议:在每个switch语句中都放一条default子句,甚至可以在后面再加一个break语句
(当switch表达式的值并不匹配所有case标签的值时,default子句后面的语句就会被执行)
(每个switch语句中只能出现一个default子句)

预处理、编译

#define 可以定义宏,宏的参数是无类型的

静态库

//导入静态库
#pragma comment(lib, "xxx.lib")

头文件相关

如果引入自己建立的头文件,则使用

#include "xxx.h"

一般将头文件与源文件分离

数组与指针

数组

数组的初始化

数组在创建的时候如果不想指定数组的确定的大小就得初始化,数组的元素个数根据初始化的内容来确定
二维数组初始化时,行数可省略,列数不可省略;
三维数组初始化时,只可以省略一维;

数组名本质上是:数组首元素的地址,二维数组的数组名也表示数组首元素的地址

数组名确实能表示首元素的地址,但是有2个例外:

  1. sizeof(array)

这里的数组名表示整个数组,计算的是整个数组的大小,单位是字节

  1. &array

这里的数组名表示整个数组,取出的是整个数组的地址

int main() {
	int arr[10] = { 0 };
	printf("%p\n", arr);
	printf("%p\n", arr+1);
	printf("---------------\n");
	printf("%p\n", &arr[0]);
	printf("%p\n", &arr[0]+1);
	printf("---------------\n");
	printf("%p\n", &arr);
	printf("%p\n", &arr+1);


	return 0;
}
//下列几种用法效果一致,都是取arr[7]的元素
arr[7] --> *(arr+7) --> *(7+arr) --> 7[arr]

数组元素的个数

int arr[10];
int size = sizeof(arr) / sizeof(arr[0]);

数组在内存中的存储

数组在内存中是连续存放的

数组越界

C语言本身是不进行数组越界的检查的,编译器也不一定会报错,所以在编写代码时,一定要自行注意

指针

  • 存放指针(地址)的变量就是指针变量
int a = 10;
int * p = &a;
/*
int 说明p指向的对象是int类型的,
* 说明p是指针变量
*/
//注意创建指针时,指针符号只作用于第一个变量
int * p1, p2, p3;  //仅有p1为指针,p2、p3为int

指针和指针类型

指针也是具有类型的,指针的定义方式为:type + *

char *pc = NULL;
int *pi = NULL:
short *ps = NULL;
long *pl = NULL;
float *pf = NULL;
double *pd = NULL;

那么指针类型的意义是什么呢?
指针的类型决定了指针向前或向后的步长,或是指针在解引用时解读的位数(即在使用指针时,在内存中以该类型的位数去解读内存中存放的内容)(以x86为例,解引用int型指针时,就会读取4个字节的数据;解引用char型指针时,就会读取1个字节的数据)

野指针

野指针就是 指针指向的位置是不可知的(或是随机的、不正确的) 指针

  • 成因
    • 指针未初始化
int main()
{ 
 int *p;//局部变量指针未初始化,默认为随机值
 *p = 20;
 return 0;
}
  • 指针越界访问
#include <stdio.h>
int main()
{
    int arr[10] = {0};
    int *p = arr;
    int i = 0;
    for(i=0; i<=11; i++)
   {
        //当指针指向的范围超出数组arr的范围时,p就是野指针
        *(p++) = i;
   }
    return 0;
}
  • 指针指向的空间释放
int main() {
    // 分配内存空间
    int* ptr = (int*)malloc(sizeof(int));

    // 检查内存是否成功分配
    if (ptr == NULL) {
        printf("内存分配失败\n");
        return 1;
    }
    else {
        printf("内存分配成功\n");
        *ptr = 2;
    }
    

    // 释放内存空间
    free(ptr);

    // 尝试访问已经释放的内存空间
    *ptr = 10;  // 这里会导致野指针

    return 0;
}
  • 如何避免野指针
    • 指针初始化
int *p = NULL;
  • 避免数组越界
  • 指针所指向的空间释放后,及时置为NULL
  • 避免返回局部变量的地址
  • 指针使用之前检查有效性(有时候即使指针不为空,它也可能指向无效的内存区域,需要根据实际情况去检查)

二级指针

指针变量也是变量,是变量就存在地址,指针变量的地址所存放的地址就是二级指针

int main() {
	int a = 10;
	int* pa = &a;  //a的地址存放在pa中,pa是一级指针
	int** ppa = &pa;  //pa的地址存放在ppa中,ppa是二级指针

	* *ppa = 20;  //先通过*ppa找到pa,再对pa进行解引用,找到了变量a

	return 0;
}

image.png

指针数组

指针数组是 存放指针的数组

数据类型

整型提升

short与char在运算时,如果进行运算,可能会发生整型提升(即将short与char转换为int进行运算,结果再截回然后再取回原位数)

关于整型提升(From. ChatGPT)
C语言中的整型提升是指在表达式中使用不同大小的整数类型时,较小的类型会被自动提升为较大的类型,以便进行运算。这个过程是隐式的,编译器在编译阶段自动执行。
整型提升的规则如下:

  1. 如果一个操作数是小于int的整数类型(如charshort等),那么它会被自动提升为int类型。
  2. 如果int类型可以容纳所有可能的值,那么int类型会被提升为unsigned int类型。
  3. 如果unsigned int也不够存储所有可能的值,那么操作数会被提升为更大的整数类型,比如longunsigned long,依次类推。

整型提升的目的是为了防止精度损失和提高表达式中的一致性。

整型提升可能会导致错误的例子通常涉及到类型不匹配的运算或比较。以下是一些使用整型提升可能导致错误的示例:

  1. 比较不同类型的整数
unsigned int ui = 100;
int i = -50;
if (ui > i) {
    printf("ui is greater than i\n");
} else {
    printf("ui is not greater than i\n");
}

在这个例子中,i是一个int类型,而ui是一个unsigned int类型。由于整型提升,i会被提升为unsigned int类型,因为这是比较的类型。然后,-50会被转换为一个很大的正数,结果是ui实际上小于i,但输出会显示 ui is greater than i,这可能会导致逻辑错误。
2. 使用char类型进行算术运算

char c = 100;
char result = c + 50;

在这个例子中,c是一个char类型,50是一个int类型。由于整型提升,c会被提升为int类型,然后进行加法运算。结果会被截断为char类型,可能导致溢出或不正确的结果。
3. 混合使用不同大小的整数类型

short s = 1000;
int i = 2000;
long l = s * i;

在这个例子中,s是一个short类型,i是一个int类型。在表达式s * i中,s会被提升为int类型,然后进行乘法运算。然而,结果会被赋给一个long类型的变量l,这可能导致精度损失或溢出。

字符类型

C语言中有字符类型(基本类型),但基本类型中没有字符串类型

字符串

字符串的结束标志位为’\0’

函数

函数定义

  • 函数可以嵌套使用,但不可以嵌套定义
  • 函数不写返回值的时候,默认返回类型是int

实参与形参

形参(形式参数)是指函数名后括号中的变量,形参只有在函数被调用的过程中才实例化(分配内存单元),当函数调用完成后,形参就自动销毁了,所以形参只在函数中有效
当实参传递给形参时,形参是实参的一份临时拷贝,对形参的修改不会影响实参

函数的调用

  • 传值调用
  • 传址调用
    • 传址调用是把函数外部创建的变量的内存地址传递给函数参数的一种调用函数的方式
    • 这种传参方式可以使函数和函数外部的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量

函数的声明

函数的声明一般出现在函数的使用之前,要先声明后使用

printf的输出格式

格式输出类型
%dint
%cchar
%sstr
%ffloat
%lfdouble
%zu常用于输出sizeof的返回值

结构体

结构体的访问

. 结构体.成员名
-> 结构体指针->成员名

//结构体的成员可以是标量、数组、指针和结构体
typedef struct Stu {
	char name[20];
	int age;
	char sex[5];
	char id[20];
}Stu; //分号不能丢

struct Point {
	int x;
	int y;
}p1;  //声明类型的同时定义变量p1

struct Point p2;  //定义结构体变量p2,有点类似面向对象的创建对象

//初始化:定义变量的同时赋初值
struct Point p3 = { 1, 2 };

struct Stu {
	char name[15];
	int age;
};

struct Stu s = { "zhangsan", 20 };

struct Node {
	int data;
	struct Point p;
	struct Node* next;
}n1 = { 10, {4, 5}, NULL }; //结构体嵌套初始化

struct Node n2 = { 20, {5, 6}, NULL };

struct Stu {
	char name[20];
	int age;
};
//结构体成员的访问
void print1(struct Stu* ps) {
	printf("name = %s age = %d\n", (*ps).name, (*ps).age);
	printf("name = %s age = %d\n", ps->name, ps->age);
}

int main() {
	struct Stu s = { "zhangsan", 20 };
	print1(&s); //结构体地址传参
	return 0;
}

特殊的声明

在声明结构体的时候,可以进行不完全的声明,可以省略掉结构体标签

注意:编译器会将上面的两个声明当成是完全不同的两个类型

struct {
	int a;
	char b;
	float c;
}x;

struct {
	int a;
	char b;
	float c;
}a[20], *p;

结构体的自引用

struct Node {
	int data;
	struct Node* next;
};

// 链表的常见创建方式
typedef struct Node {
	int data;
	struct Node* next;
}Node;

结构体的内存对齐

结构体的对齐规则:

  1. 第一个成员在结构体变量偏移量为0的地址处
  2. 其他成员变量要对其到某个数字(对齐数)的整数倍地址处
    1. 实际对齐数 为编译器默认的对齐数与该成员大小的较小值
  3. 结构体总大小为最大对齐数的整数倍
  4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍,结构体的整体大小是所有最大对齐数(包含结构体)的整数倍

可以参照这内存对齐的知识进行理解,都是利用空间换取时间,尽可能减少读取的次数

所以在设计结构体时,既要满足对齐,又要节省空间,就要尽可能让占用空间小的成员集中在一起,减少对齐导致的空间浪费

struct S1 {
	char c1;
	int i;
	char c2;
};

struct S2 {
	char c1;
	char c2;
	int i;
};

struct S3 {
	double d;
	char c;
	int i;
};

struct S4 {
	char c1;
	struct S3 s3;
	double d;
};

int main() {
	printf("%d\n", sizeof(struct S1));
	printf("%d\n", sizeof(struct S2));
	printf("%d\n", sizeof(struct S3));
	printf("%d\n", sizeof(struct S4));
	return 0;
}

枚举

枚举的优点

  1. 增加代码的可读性和可维护性
  2. 和#define定义的标识符比较,枚举有类型检查
  3. 防止命名污染
  4. 便于调试
  5. 使用方便,一次可定义多个常量
enum Day {
	Mon,
	Tues,
	Wed,
	Thur,
	Fri,
	Sat,
	Sun
};

enum Sex {
	MALE,
	FEMALE,
	SECRET
};

enum Color {
	RED=1,
	GREEN=2,
	BLUE=4
};

enum Color clr = GREEN;

联合

联合类型的定义

联合是一种特殊的自定义类型,特征是这些成员共用同一块空间,这样一个联合变量的大小,至少是最大成员的大小

union Un {
	int i;
	char c;
};
union Un un;

int main() {
	printf("%p\n", &(un.i));
	printf("%p\n", &(un.c));

	un.i = 0x11223344;
	un.c = 0x55;
	printf("%x\n", un.i);

	return 0;
}

分别取出un.i与un.c的地址,发现两者相同
image.png
令un.i = 0x11223344
image.png
再令un.c = 0x55,发现原先un.i中的头两位的0x11被覆盖了
image.png

联合大小的计算

  1. 联合的大小至少是最大成员的大小
  2. 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍
union Un1 {
	char c[5];
	int i;
};
union Un2 {
	short c[7];
	int i;
};

int main() {
	printf("%d\n", sizeof(union Un1));
	printf("%d\n", sizeof(union Un2));
	return 0;
}

动态内存

  • 为什么要使用动态内存?

因为静态内存开辟的空间大小是固定的,但是有的时候,我们所需要的空间大小需要在程序运行时才可知,所以就需要用到动态内存

malloc & free

image.png
image.png

image.png

int main() {
	int num = 0;
	scanf("%d", &num);
	//int arr[num] = { 0 };  //该表达式中num必须为常量值

	int* ptr = NULL;
	ptr = (int*)malloc(num * sizeof(int));
	int i = 0;
	if (NULL != ptr) {
		for (i = 0; i < num; i++) {
			*(ptr + i) = i;
		}
	}
	for (int i = 0; i < num; i++) {
		printf("%d ", *(ptr + i));
	}
	free(ptr);
	ptr = NULL;

	return 0;
}

代码辨析(易错点)

// 输出为 1 2 3 4
int main() {
	int i = 1;
	while (i <= 10) {
		if (i == 5)
			continue;
		printf("%d ", i);
		i = i + 1;
	}
	return 0;
}

// 输出为 2 3 4 6 7 8 9 10
int main() {
	int i = 1;
	while (i <= 10) {
		i = i + 1;
		if (i == 5)
			continue;
		printf("%d ", i);
	}
	return 0;
}
//此处的代码适当修改后可用于清理缓冲区
int main() {
	int ch = 0;
	while ((ch = getchar()) != EOF) {
		putchar(ch);
	}
	return 0;
}

//只打印数字字符,跳过其他字符
int main() {
	char ch = '\0';
	while ((ch = getchar()) != EOF) {
		if (ch < '0' || ch > '9') {
			continue;
		}
		putchar(ch);
	}

	return 0;
}
// 计算n的阶乘
int main() {
	int n = 0;
	printf("Please input num n: ");
	scanf("%d\n", &n); // may have error
	if (n < 0) {
		printf("输入的数据为负数");
	}
	else if (n == 0) {
		printf("n! = 1");
	}
	else {
		for (int i = n - 1; i > 1; i--) {
			n = n * i;
			printf("%d\n", n);
		}
		printf("n! = %d\n", n);
	}

	return 0;
}

//suggestion: Removed '\n' from the scanf format string to avoid skipping whitespace characters.
/* 
reason(from ChatGPT):
In C, the scanf function reads input from the standard input stream (usually the keyboard) based on the format specifier provided. When you use %d in scanf, it expects an integer input. However, %d doesn't consume the newline character (\n) from the input stream.

So when you input an integer followed by pressing Enter, the integer is read by %d, but the newline character remains in the input buffer. If the next scanf call in your code uses %d again, it will read this newline character as input, which can lead to unexpected behavior.

By removing '\n' from the scanf format string, you ensure that scanf only reads integers and doesn't consume the newline character, leaving it in the buffer. This prevents issues when subsequent scanf calls or other input reading functions are used in your code.
*/

  • 28
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值