程序设计|C语言教学——C语言基础4:进阶

一、预处理指令

预处理指令在编译前执行,除了#include,还有以下常用指令:

1. #define 宏定义
  • 无参宏:定义常量或代码片段,编译时直接替换(无类型检查)。

#define PI 3.1415926  // 定义常量
#define MAX(a,b) (a > b ? a : b)  // 定义简单逻辑(注意括号,避免运算优先级问题)
  • 注意:宏替换是 “文本替换”,可能导致副作用,例如 MAX(a++, b++) 会导致ab多自增一次。

  • 带参宏与函数的区别

    特性带参宏函数
    执行时机编译前替换运行时调用
    开销无调用开销(代码膨胀)有栈帧开销
    类型检查
    返回值无(替换结果直接使用)有明确返回值类型
2. 条件编译

用于控制代码是否参与编译,常用于跨平台开发或调试。

#define DEBUG 1  // 定义宏DEBUG#ifdef DEBUG  // 如果定义了DEBUG,则编译以下代码printf("调试信息:变量x的值为%d\n", x);
#else  // 否则编译以下代码// 无调试信息
#endif#ifndef _HEADER_H  // 如果未定义_HEADER_H(防止头文件重复包含)
#define _HEADER_H
// 头文件内容
#endif

二、数据类型进阶

1. 类型修饰符
  • short/long:修饰整数长度(short int 通常 2 字节,long int 通常 4/8 字节,取决于系统)。
  • signed/unsignedsigned int 可表示正负(默认),unsigned int 只表示非负(范围更大)。
unsigned int num = 10;  // 范围:0 ~ 4294967295(32位系统)
signed char c = -12;    // 范围:-128 ~ 127(8位)
2. 自定义类型
  • 结构体(struct:组合不同类型数据,用于描述复杂对象(如学生、坐标)。

// 定义结构体类型,此时的student是一种自定义的新数据类型,像int一样
struct Student {char name[20];  // 姓名int age;        // 年龄float score;    // 成绩
};int main() {// 声明结构体变量并初始化struct Student stu = {"张三", 18, 90.5f};// 访问成员(用.运算符)printf("姓名:%s,年龄:%d\n", stu.name, stu.age);// 结构体指针(用->运算符访问成员)struct Student *p = &stu;p->score = 95.0f;  // 等价于 (*p).score = 95.0freturn 0;
}
  • 枚举(enum:定义命名的整数常量,提高代码可读性。
enum Weekday {MON,  // 默认为0TUE,  // 1WED=5,  // 显式赋值5THU   // 6(自动递增)
};int main() {enum Weekday day = WED;printf("%d\n", day);  // 输出5return 0;
}
  • 共用体(union:所有成员共享同一块内存(大小为最大成员的大小),用于节省空间。
union Data {int i;float f;char c;
};  // 大小为4字节(float和int通常4字节)int main() {union Data d;d.i = 10;printf("d.i = %d, d.f = %f\n", d.i, d.f);  // f的值会混乱(内存被i覆盖)return 0;
}
3. 结构体的高级用法
  • 结构体嵌套与自引用(链表基础):结构体可嵌套其他结构体,自引用(包含自身类型的指针)是实现链表、树等数据结构的核心。

示例:单向链表节点

#include <stdio.h>
#include <stdlib.h>// 结构体自引用(必须用指针,否则会无限递归定义)
struct Node {int data;  // 数据域struct Node *next;  // 指针域:指向 next 节点
};// 创建新节点
struct Node* createNode(int data) {struct Node *newNode = (struct Node*)malloc(sizeof(struct Node));newNode->data = data;newNode->next = NULL;  // 初始指向NULLreturn newNode;
}int main() {// 创建3个节点并链接struct Node *n1 = createNode(10);struct Node *n2 = createNode(20);struct Node *n3 = createNode(30);n1->next = n2;  // n1 指向 n2n2->next = n3;  // n2 指向 n3// 遍历链表struct Node *current = n1;while (current != NULL) {printf("%d ", current->data);  // 输出:10 20 30current = current->next;}// 释放链表内存(从首节点依次释放)current = n1;while (current != NULL) {struct Node *temp = current;current = current->next;free(temp);}return 0;
}

三、函数与指针深入

1. 函数参数与返回值
  • 传值调用:函数接收参数的副本,修改副本不影响原变量。

void swap(int a, int b) {int temp = a;a = b;b = temp;  // 仅修改副本,原变量不变
}
  • 传址调用:通过指针传递变量地址,函数可修改原变量。
void swap(int *a, int *b) {int temp = *a;*a = *b;*b = temp;  // 直接修改原变量
}int main() {int x=3, y=5;swap(&x, &y);  // 传递地址printf("x=%d, y=%d\n", x, y);  // 输出x=5, y=3return 0;
}
  • 指针函数(返回指针的函数):返回值为指针(需注意:不能返回局部变量的地址,因其生命周期随函数结束而结束)。
int* getStaticPtr() {static int num = 10;  // static变量生命周期为整个程序,地址有效return &num;
}
  • 函数指针(指向函数的指针):函数指针存储函数的地址,可实现 “回调函数”(将函数作为参数传递),是 C 语言实现多态的重要方式。
#include <stdio.h>// 加法函数
int add(int a, int b) { return a + b; }// 乘法函数
int multiply(int a, int b) { return a * b; }// 函数指针作为参数(回调函数)
int calculate(int a, int b, int (*func)(int, int)) {return func(a, b);  // 调用传入的函数
}int main() {int x=3, y=4;// 用函数指针调用不同函数,实现不同逻辑printf("加法结果:%d\n", calculate(x, y, add));       // 7printf("乘法结果:%d\n", calculate(x, y, multiply)); // 12return 0;
}
2. 函数递归

函数自身调用自身,需满足终止条件递归关系(如阶乘、斐波那契数列)。

// 计算n的阶乘:n! = n * (n-1)!,终止条件n=1时返回1
int factorial(int n) {if (n == 1) return 1;  // 终止条件return n * factorial(n-1);  // 递归关系
}

注意:递归深度过大会导致栈溢出(栈内存有限),复杂场景建议用循环替代。

3. 指针深入
  • 二级指针(指针的指针):用于处理 “指针的集合”(如动态二维数组、指针数组的修改)。

示例:动态创建二维数组(用二级指针)

#include <stdio.h>
#include <stdlib.h>int main() {int rows = 2, cols = 3;int **arr;  // 二级指针:指向int*类型的指针// 第一步:分配存放指针的内存(rows个int*)arr = (int**)malloc(rows * sizeof(int*));// 第二步:为每个指针分配数组内存for (int i=0; i<rows; i++) {arr[i] = (int*)malloc(cols * sizeof(int));}// 赋值arr[0][0] = 1; arr[0][1] = 2; arr[0][2] = 3;arr[1][0] = 4; arr[1][1] = 5; arr[1][2] = 6;// 打印for (int i=0; i<rows; i++) {for (int j=0; j<cols; j++) {printf("%d ", arr[i][j]);}printf("\n");}// 释放内存(先释放内层,再释放外层)for (int i=0; i<rows; i++) {free(arr[i]);}free(arr);return 0;
}
  • const 修饰的指针:区分 “指向 const 的指针” 和 “const 指针”,避免意外修改数据。
#include <stdio.h>int main() {int a = 10, b = 20;// 1. 指向const的指针(不能通过指针修改指向的值,但指针可指向其他地址)const int *p1 = &a;// *p1 = 30;  // 错误:不能修改指向的值p1 = &b;  // 正确:可指向其他地址// 2. const指针(指针本身不能修改指向,但可修改指向的值)int *const p2 = &a;*p2 = 30;  // 正确:可修改指向的值// p2 = &b;  // 错误:指针本身是const,不能改指向// 3. 指向const的const指针(既不能改指向,也不能改值)const int *const p3 = &a;return 0;
}

四、数组与指针深入

1. 数组与指针的关系

数组名本质是首元素地址(常量指针,不可修改),可通过指针访问数组元素。

int arr[5] = {1,2,3,4,5};
int *p = arr;  // 等价于 p = &arr[0]// 访问arr[2]的三种方式
printf("%d\n", arr[2]);    // 数组下标
printf("%d\n", *(arr+2));  // 数组名+偏移量
printf("%d\n", *(p+2));    // 指针+偏移量
2. 字符数组与字符串

字符串是以'\0'(ASCII 值 0)结尾的字符数组,printfstrlen等函数通过'\0'判断结束。

char str1[] = "hello";  // 自动添加'\0',长度为6(h e l l o \0)
char str2[] = {'h','i','\0'};  // 必须显式添加'\0',否则不是字符串// 字符串处理函数(需包含<string.h>)
printf("长度:%d\n", strlen(str1));  // 输出5(不包含'\0')
char dest[20];
strcpy(dest, str1);  // 复制字符串(注意dest容量足够)
strcat(dest, " world");  // 拼接字符串
3. 指针数组与数组指针
  • 指针数组:数组元素是指针(如存储多个字符串的地址)。

char *strs[] = {"apple", "banana", "cherry"};  // 每个元素是字符串常量的地址
for (int i=0; i<3; i++) {printf("%s\n", strs[i]);  // 输出三个字符串
}
  • 数组指针:指向整个数组的指针(需指定数组长度)。
int arr[3] = {1,2,3};
int (*p)[3] = &arr;  // p指向包含3个int的数组
printf("%d\n", (*p)[1]);  // 输出2(访问数组第2个元素)

也就是说,指针可以指向数组的某个元素,也可以指向整个数组,也可以作为元素本身构成数组。当指针直接指向数组名的时候,存储的实际上是数组的第一个元素的地址。

4. 多维数组的指针访问

二维数组在内存中是连续存储的(按行优先排列),可通过指针灵活访问,而不仅限于arr[i][j]的形式。

示例:用指针遍历二维数组

#include <stdio.h>int main() {int arr[2][3] = {{1,2,3}, {4,5,6}};int *p = &arr[0][0];  // 指向首元素的指针(或直接用 int *p = arr[0];)// 遍历整个二维数组(共2×3=6个元素)for (int i=0; i<2*3; i++) {printf("%d ", *(p+i));  // 输出:1 2 3 4 5 6}return 0;
}
  • 二维数组名arr数组指针(类型为int (*)[3]),指向第一行的整个数组,arr+1会跳过一整行(3 个 int)。
  • 易错点:arr[i][j]等价于*(*(arr+i)+j),需注意指针类型匹配。

五、内存管理

C 语言需手动管理内存,通过stdlib.h中的函数操作堆内存(堆内存需手动申请和释放,否则内存泄漏)。

1. 动态内存分配
  • malloc(size):分配size字节的内存,返回void*(需强制类型转换),失败返回NULL
  • calloc(n, size):分配nsize字节的内存,初始化为 0。
  • realloc(ptr, new_size):调整已分配内存的大小,可能移动内存块。
2. 内存释放
  • free(ptr):释放malloc/calloc/realloc分配的内存,释放后ptr应置为NULL(避免野指针)。

示例:

#include <stdio.h>
#include <stdlib.h>int main() {// 分配10个int的内存(40字节)int *arr = (int*)malloc(10 * sizeof(int));if (arr == NULL) {  // 检查分配是否成功printf("内存分配失败\n");return 1;}// 使用内存for (int i=0; i<10; i++) {arr[i] = i;}// 重新分配为20个int的内存int *new_arr = (int*)realloc(arr, 20 * sizeof(int));if (new_arr != NULL) {arr = new_arr;  // 重新分配成功,更新指针}// 释放内存free(arr);arr = NULL;  // 避免野指针return 0;
}

六、文件操作

通过stdio.h中的函数读写文件,核心是文件指针(FILE*

1. 文件打开与关闭
  • fopen(filename, mode):打开文件,mode为打开模式("r"读,"w"写,"a"追加等),失败返回NULL
  • fclose(fp):关闭文件,必须调用(否则可能丢失数据)。
2. 文件读写
  • 字符级:fgetc(fp)(读一个字符)、fputc(c, fp)(写一个字符)。
  • 字符串级:fgets(buf, size, fp)(读一行)、fputs(str, fp)(写字符串)。
  • 格式化:fscanf(fp, format, ...)fprintf(fp, format, ...)
#include <stdio.h>int main() {// 写文件FILE *fp = fopen("test.txt", "w");  // 以写模式打开if (fp == NULL) {printf("文件打开失败\n");return 1;}fprintf(fp, "Hello, File!\n");  // 写入内容fclose(fp);  // 关闭文件// 读文件fp = fopen("test.txt", "r");char buf[100];fgets(buf, 100, fp);  // 读取一行printf("读取内容:%s", buf);  // 输出Hello, File!fclose(fp);return 0;
}

七、其他的一些名词

  • 野指针:访问已释放或未初始化的指针(可能崩溃)。
  • 数组越界:访问超出数组长度的元素(可能修改其他内存)。
  • 内存泄漏:未用free释放堆内存(长期运行的程序会耗尽内存)。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/diannao/95747.shtml
繁体地址,请注明出处:http://hk.pswp.cn/diannao/95747.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

数据结构之heap算法

文章目录前言1. heap结构概述2. push_heap3. pop_heap4. sort_heap5. make_heap前言 heap这种数据结构&#xff0c;允许用户以任何次序将任何数据放入该结构中&#xff0c;但是最后取出数据的时候一定是权值最高&#xff08;或者最低&#xff09;的元素。主要和实现有关&#x…

MCU 软件断点调试注意事项!!!

——为什么调试器会在运行中改我的Flash程序&#xff1f;调试单片机时&#xff0c;很多人都有这样的疑问&#xff1a;明明我在调试前刷进去的固件是好的&#xff0c;为什么加了一个断点之后&#xff0c;调试器居然去改了 Flash&#xff1f; 如果我拔掉调试器&#xff0c;这个固…

启发式合并 + 莫队 恋恋的心跳大冒险

题目来源&#xff1a;2025 Wuhan University of Technology Programming Contest 比赛链接&#xff1a;Dashboard - 2025 Wuhan University of Technology Programming Contest - Codeforces 题目大意&#xff1a; Solution&#xff1a; 首先肯定要预处理出以每个节点为起点…

JCTools 无锁并发队列基础:ConcurrentCircularArrayQueue

ConcurrentCircularArrayQueue ConcurrentCircularArrayQueue 是一个抽象类&#xff0c;它为基于数组的并发循环队列提供了基础功能。从其命名可以看出几个关键特性&#xff1a;​​Concurrent​​&#xff1a;常指无锁并发。​​Circular Array​​&#xff1a;内部使用循环数…

力扣(LeetCode) ——622. 设计循环队列(C语言)

题目&#xff1a;622. 设计循环队列示例1&#xff1a; MyCircularQueue circularQueue new MyCircularQueue(3); // 设置长度为 3 circularQueue.enQueue(1); // 返回 true circularQueue.enQueue(2); // 返回 true circularQueue.enQueue(3); // 返回 true circularQueue.…

在JVM跑JavaScript脚本 | Oracle GraalJS 简介与实践

这是2024年初的 GraalVM 系列博文&#xff0c;当时写了大纲&#xff0c;知道一年半后的现在才得以完成发布&#x1f604; 1、概述 实话说&#xff0c;标题的场景为小众需求&#xff0c;日常开发基本用不到&#xff0c;我是最近在做一个低代码轮子玩具 app-meta 需要实现 FaaS&…

基于 EC 数据与大模型技术实现天气预报:从数据到上线的全栈方法

1. 先校准“EC 数据”与“AI 预报”的语境 EC 数据家族(常用) IFS/HRES:确定性全球模式,水平分辨率约 9 km,常用预报范围 10 天; IFS/ENS:51 成员集合预报,提供 15 天概率信息; ERA5:再分析数据,小时级、0.25,可追溯至 1940 年,用作训练/评测黄金基准。 AI 预报…

迭代器模式及优化

迭代器模式&#xff08;Iterator Pattern&#xff09;是一种行为型设计模式&#xff0c;用于提供一种统一的方式遍历聚合对象&#xff08;如集合、容器&#xff09;中的元素&#xff0c;而无需暴露对象的内部实现细节。它将遍历逻辑与聚合对象分离&#xff0c;使得遍历操作可以…

纯Qt手撕gb28181协议/gb28181协议服务端/gb28181协议设备端/gb28181设备模拟器/gb28181虚拟监控设备

一、前言说明 搞完onvif设备模拟器&#xff0c;总想着把28181设备模拟也实现&#xff0c;因为之前已经花了大力气把28181平台软件端实现了&#xff0c;为了实现这个组件&#xff0c;头发掉了一大把&#xff0c;专门把国标文档看了好几遍&#xff0c;逐行阅读&#xff0c;针对需…

【渗透实战】无下载器环境(curl/wget)下玩转 Metasploit 自动利用

1. 背景与问题场景 在渗透测试或漏洞利用中&#xff0c;Metasploit&#xff08;MSF&#xff09;是业界最常用的框架之一。 其许多 RCE&#xff08;远程代码执行&#xff09;模块在落地 payload&#xff08;如 Meterpreter 或反弹 shell&#xff09;时&#xff0c;采用了 CMD St…

jd-hotkey探测热点key

对任意突发性的无法预先感知的热点数据&#xff0c;包括并不限于热点数据&#xff08;如突发大量请求同一个商品&#xff09;、热用户&#xff08;如恶意爬虫刷子&#xff09;、热接口&#xff08;突发海量请求同一个接口&#xff09;等&#xff0c;进行毫秒级精准探测到。然后…

C#WPF实战出真汁07--【系统设置】--菜品类型设置

1、菜品设置介绍 菜品设置跟餐桌设置的功能目的是相同的&#xff0c;包括了新增&#xff0c;删除&#xff0c;编辑&#xff0c;分页&#xff0c;查询&#xff0c;重置&#xff0c;全选&#xff0c;全消&#xff0c;列表功能&#xff0c;实现流程也是布局设计&#xff0c;后台逻…

aave v3 存款与借款利息的计算方式

本文只涉及到利率计算的数学原理&#xff0c;不作源码解析:存款首先我们假设小明在aave里面存了10000usdt&#xff0c;存的时候年化收益率是5%,那么半年后其存款的利息是多少呢?常规的计算方式如下:利息10000*5%*(存款的时长/一年的时长)这么做有什么问题呢&#xff1f;假设现…

Windows MCP.Net:基于.NET的Windows桌面自动化MCP服务器深度解析

&#x1f4cb; 目录 项目概述 技术架构深度解析 核心功能模块详解 代码实现分析 使用场景与实战案例 性能优化与最佳实践 扩展开发指南 总结与展望 项目概述 什么是Windows-MCP.Net&#xff1f; Windows MCP.Net是一个基于.NET 10.0开发的Windows桌面自动化MCP&…

Boost.Asio学习(7):Boost.Beast实现简易http服务器

namespace beast boost::beast;beast::flat_buffer是一个用于 Boost.Asio 和 Boost.Beast 网络读写的缓冲区实现。专为 一次性顺序读取 / 消费 场景设计&#xff0c;比 std::string 或 std::vector 高效&#xff0c;因为它是扁平内存结构&#xff08;contiguous memory&#x…

深入解析JVM内存区域划分:从理论到实践

Java虚拟机&#xff08;JVM&#xff09;是Java程序运行的核心环境&#xff0c;它负责管理内存分配、垃圾回收、字节码执行等关键任务。理解JVM的内存区域划分&#xff0c;对于优化Java应用性能、排查内存问题&#xff08;如OutOfMemoryError、StackOverflowError&#xff09;至…

滑窗|贪心|✅滚动数组

lc17.08pair按身高升序、相同时体重降序排序结果是找体重序列的最长递增子序列长度核心&#xff1a;转化为二维最长递增子序列问题求解vector<int> dp;for (auto& p : hw) {int w p.second;auto it lower_bound(dp.begin(), dp.end(), w);if (it dp.end()) {dp.pu…

深入理解数据库架构:从原理到实践的完整指南

一、数据库存储架构的多维度分类体系 1.1 基于数据组织方式的存储架构分类 数据库的存储架构从根本上决定了其性能特征、适用场景和扩展能力。理解不同的数据组织方式是选择合适数据库技术的基础&#xff0c;这种分类不仅反映了技术实现的差异&#xff0c;更体现了对不同业务需…

体彩排列三第2025218期号码分析

大家好&#xff0c;本人蔡楚门来此平台分享一下本期得经验和思路&#xff0c;希望能够给大家带来好的运气和灵感&#xff01;体彩排列三第2025218期号码分析&#xff0c;大小号码数字分析&#xff0c;上期开出全小号码最多&#xff0c;最近两期的开奖号码全部都是全小号码最多&…

java设计模式之迪米特法则介绍与说明

一、核心概念与目标 基本定义 迪米特法则的核心思想是&#xff1a;一个对象应该对其他对象尽可能少地了解&#xff0c;仅与直接关联的对象&#xff08;即“朋友”&#xff09;通信&#xff0c;避免与“陌生人”产生直接交互。 直接朋友&#xff1a;包括当前对象的成员变量、方法…