从0到1构造链表
#define N 20
typedef enum{
Male,
Female
}gender_type;
typedef struct stu{
char name[N];
unsigned char age;
gender_type gender;
struct stu *next;
}student;
void print_info(student *p){
while(p != NULL){
printf("%s:%d %d\n",p ->name,p ->age, p -> gender);
p = p -> next;
}
}
#include<stdio.h>
int main(){
student s1 = {"1-zhangsan", 20, Male, NULL};
student s2 = {"2-lisi", 21, Female, NULL};
student s3 = {
.name = "3-wangwu",
.age = 19,
.gender = Male,
.next = NULL
};//无输入类型
s1.next = &s2;
s2.next = &s3;
print_info(&s1);
return 0;
}
从0到1构造链表
本文综合很多知识,适合基础不牢的同学看,基本功扎实的同学可以直接跳到链表
静态链表
结构体
- 概念
结构体(struct)是一种用户自定义的数据类型,它允许你将不同类型的数据组合在一起 - 定义
struct Student {
①char name[50];//字符数组
int age;
float grade;
};
- 初始化
定义同时初始化(适用于全局变量和静态变量情况)
对于全局或静态的结构体变量,可以在定义时进行初始化
// 这里直接按照结构体成员定义的顺序,用相应类型的值来初始化各个成员。
struct Student s1 = {"Alice", 20, 90.5};
//也可以指定具体成员进行初始化,不依赖顺序,C99支持
struct Student s3 = {.score = 92.0,.age = 21,.name = "Charlie"};
struct Student s3 = {.score = 92.0,.name = "Charlie"};
- 结构体的输入[可以不用初始化]
逐个成员输入(通过 scanf 等函数)
可以使用scanf
等输入函数对结构体的各个成员进行逐个输入,例如:
#include <stdio.h>
struct Student {
char name[50];
int age;
float score;
};
int main() {
struct Student s;
printf("请输入学生姓名:");
scanf("%s", s.name);
printf("请输入学生年龄:");
scanf("%d", &s.age);
printf("请输入学生成绩:");
scanf("%f", &s.score);
return 0;
}
注意,对于字符数组类型的成员(如 name
),在使用 scanf
输入时,不需要取地址符 &
,因为数组名本身就代表首地址其他照旧
使用函数辅助输入(提高代码复用性和可读性)
可以编写一个函数专门用于输入结构体变量的信息,例如:
#include <stdio.h>
struct Student {
char name[50];
int age;
float score;
};
void inputStudent(struct Student *s) {
printf("请输入学生姓名:");
scanf("%s", s->name);
printf("请输入学生年龄:");
scanf("%d", &s->age);
printf("请输入学生成绩:");
scanf("%f", &s->age);
}
int main() {
struct Student s;
inputStudent(&s);
return 0;
}
这里通过指针传递结构体变量的地址给函数,在函数内部使用指针操作符 ->
来访问结构体成员进行输入操作。
- 结构体的输出
- 逐个成员输出(通过 printf 等函数)
同样可以使用printf
等输出函数对结构体的各个成员进行逐个输出,示例如下:
#include <stdio.h>
struct Student {
char name[50];
int age;
float score;
};
int main() {
struct Student s = {"David", 23, 95.0};
printf("学生姓名:%s\n", s.name);
printf("学生年龄:%d\n", s.age);
printf("学生成绩:%f\n", s.score);
return 0;
}
- 自定义函数输出(便于统一格式等)
编写一个函数来输出结构体变量的信息,使输出格式更规范、统一,示例如下:
#include <stdio.h>
struct Student {
char name[50];
int age;
float score;
};
void outputStudent(struct Student s) {
printf("姓名:%s,年龄:%d,成绩:%f\n", s.name, s.age, s.score);
}
int main() {
struct Student s = {"Eve", 24, 98.0};
outputStudent(s);
return 0;
}
你大概懂了?做两道小题试试
一.
😀结构体
#include <stdio.h>
typedef struct Book{
char title[100];
char author[50];
float price;
}book;
void input(book* p){
printf("please put your information");//字符串必须用双引号!
scanf("%s %s %f",p ->title, p ->author, &p -> price);
}
//&p -> price//!!!!
//第一次写成 p -> & price
// 但这是语法错误的,因为 -> 后面应该直接跟成员名,而不是取地址操作符 &。
void output(book* p){
printf("%s %s %f",p ->title, p ->author, p ->price);
}
int main() {
book *p, i;
// *p = i ;这个不是正确的初始化
p = &i;//!!!
input(p);
output(p);
return 0;
}
😀结构体数组
#define N 5
#include <stdio.h>
typedef struct Book{
char title[100];
char author[50];
float price;
}book;
void input(book* p){
printf("please put your information");
scanf("%s %s %f",p ->title, p ->author, &p -> price);
}
void output(book* p){
printf("%s %s %0.2f\n",p ->title, p ->author, p ->price);
}
int main() {
book *p;
book arr[N];
p = arr;
int i;
for(i=0;i<3;i++){
input(p + i);
output(p + i);
}
return 0;
}
😀直接用.访问,不用函数
#define N 5
#include <stdio.h>
typedef struct Book{
char title[100];
char author[50];
float price;
}book;
void output(book* p){
printf("%s %s %0.2f\n",p ->title, p ->author, p ->price);
}
int main() {
book *p;
book arr[N];
p = arr;
int i;
printf("please put your information");//字符串必须用双引号!
for(i=0;i<3;i++){
scanf("%s %s %f",arr[i].title, arr[i].author, &arr[i].price);
}
for(i=0;i<3;i++){
output(p + i);
}
return 0;
}
显然,用函数指针传参更方便捏
二.
#define N 3
#include<stdio.h>
typedef struct employee{
int id;
char name[50];
char department[30];
int salary;
}Employee;
inputEmployee(Employee *p){
scanf("%d %s %s %d",&p -> id, p -> name, p -> department, &p ->salary);
}
outputEmployee(Employee *p){
printf("%d %s %s %d",p -> id, p -> name, p -> department, p ->salary);
}
int main(){
Employee a[N];
Employee * p;
int i;
p = a;
for(i=0;i<N;i++){
inputEmployee(p + i);
}
for(i=0;i<N;i++){
outputEmployee(p + i);
}
return 0;
}
①
在C语言中,name[50] 表示定义了一个名为 name 的 字符数组,长度为50
字符数组里可以输多少汉字字母标点符号?
-
英文字母:每个英文字母(例如 'a', 'b', 'c' 等)占用1 个字符的位置。
-
汉字:如果使用的是单字节编码(如 ASCII,但这不适用于汉字),每个汉字将无法正确存储。在多字节编码(如 UTF-8)中,一个汉字通常会占用 3 到 4 个字符的位置。
-
标点符号:大多数标点符号(例如
'.', ',', '!'
等)每个占用 1个字符的位置。
字符数组和字符串
语法
-
定义:
char myArray[10];
char 数组名[数组长度];
数组长度是一个常量 -
-
无输入的初始化:
- 可以在定义时对字符数组进行初始化:
char myArray[6] = {'H', 'e', 'l', 'l', 'o', '\0'};
- 使用字符串进行初始化:
char myArray[] = "Hello";
这里,"Hello"字符串末尾的空字符'\0'会自动添加
- 可以在定义时对字符数组进行初始化:
-
无初始化的输入
- scanf函数或其变体来输入字符数组:
scanf("%s", myArray);
但要注意,%s格式化字符串无法读取 空白字符(空格、制表符或换行符),并且不会检查数组边界,这可能导致缓冲区溢出。因此,更安全的做法是限制读取的字符数:
scanf("%9s", myArray); // 读取最多9个字符,留一个位置给'\0'
2.【推荐使用】 fgets(数组,sizeof(数组),stdin);处理scanf不可以解决的空格【空白字符输入问题】
- scanf函数或其变体来输入字符数组:
-
p.s.由于 gets
的不安全性,现代 C 程序应该避免使用它,而应该使用 fgets
或其他更安全的字符串读取函数。
#include <stdio.h>
int main() {
char buffer[100]; // 创建一个足够大的字符数组来存储读取的行
// 从标准输入读取一行,最多读取 99 个字符(最后一个位置留给 '\0')
fgets(buffer, sizeof(buffer), stdin);
// 输出读取的内容
printf("You entered: %s", buffer);
return 0;
}
- 输出
- 按照字符串形式输出:
printf("%s\n", myArray);
- 按照字符数组格式
for(i=0;i<5;i++){ printf("%c",myArray[i]); }
- 按照字符串形式输出:
字符串 === last-child是\0
的字符数组
字符串和字符数组的不同运用场景
存在即合理
- 字符数组的使用场景
- 😀存储固定长度或长度受限的文本序列
- 当你确切知道要存储的文本长度时,字符数组是很好的选择。例如,存储日期格式“YYYY - MM - DD”,其长度固定为10(包括分隔符和结尾的空字符)。
char date[11] = "2024-01-01";
- 或者存储一个人的身份证号码,长度一般为18位,也可以使用字符数组。
- 对内存使用有严格控制的情况
- 如果你的程序运行在资源受限的环境中,需要精确控制内存分配,字符数组很有用。你可以提前计算出所需的字符数组大小,避免不必要的内存浪费。例如,在嵌入式系统中存储设备配置信息,如设备名称(假设最长10个字符)和IP地址(最长15个字符)。
char deviceName[11]; char ipAddress[16];
- 这样可以确保每个字符数组只占用刚好足够的内存空间。
- 😀需要对字符逐个处理的情况
- 当你需要对每个字符进行单独的操作,如加密、解密算法,或者统计字符出现的频率等操作时,字符数组更方便。例如,对一个字符数组中的字符进行简单的加密,将每个字符的ASCII值加1。
char message[] = "hello"; for (int i = 0; i < strlen(message); i++) { message[i] = message[i] + 1; } printf("%s", message);
- 😀存储固定长度或长度受限的文本序列
- 字符串的使用场景
- 动态文本内容的存储和操作
- 当文本内容的长度不确定,且可能会动态变化时,字符串更合适。例如,在文本编辑器程序中,用户输入的文本长度是不确定的。C语言中可以使用动态分配内存的字符串来处理这种情况。
char *text = (char *)malloc(100 * sizeof(char)); // 可以根据用户输入动态扩展内存
- 这里的
text
可以根据实际输入情况,通过realloc
等函数来动态扩展内存,以适应不断变化的文本长度。
- 😀作为函数参数和返回值传递文本信息
- 很多函数需要传递文本信息,使用字符串作为参数更加方便和直观。例如,在文件读取函数
fgets
中,它接受一个字符串(字符数组)作为参数来存储读取到的行。
char line[100]; fgets(line, sizeof(line), stdin);
- 同样,函数返回值也可以是字符串。例如,一个函数返回一个错误消息字符串,方便调用者获取并处理错误信息。
- 很多函数需要传递文本信息,使用字符串作为参数更加方便和直观。例如,在文件读取函数
- 😀与字符串处理库函数配合使用
- C语言有许多强大的字符串处理函数,如
strcpy
、strcat
、strcmp
等。当你需要进行字符串的复制、拼接、比较等操作时,使用字符串(字符数组)作为这些函数的操作对象是很自然的选择。
char str1[50] = "hello"; char str2[50] = " world"; strcat(str1, str2); printf("%s", str1);
- C语言有许多强大的字符串处理函数,如
- 动态文本内容的存储和操作
四大字符串函数和运用场景
在 C 语言中,标准库 <string.h> 提供了许多用于处理字符串的函数。
strlen(s)
strcpy(s1,s2)
strcmp(s1,s2)
strcat(s1,s2)
1. 字符串长度
❤strlen(s): 返回字符串 s 的长度,不包括结尾的空字符 \0。
运用场景:确定字符串中字符的数量,或者在分配内存之前检查字符串的长度。
2. 字符串复制
❤ strcpy(s1, s2): 将字符串 s2 复制到字符串 s1。
运用场景:在需要创建字符串副本时使用。
strncpy(s1, s2, n): 将字符串 s2 的前 n 个字符复制到字符串 s1。
运用场景:当需要限制复制的字符数量时,比如防止缓冲区溢出。
3. 字符串连接
❤strcat(s1, s2): 将字符串 s2 连接到字符串 s1 的末尾。
运用场景:当需要将两个字符串合并为一个时。
strncat(s1, s2, n): 将字符串 s2 的前 n 个字符连接到字符串 s1 的末尾。
运用场景:当需要限制连接的字符数量时。
4. 字符串比较
❤strcmp(s1, s2): 比较字符串 s1 和 s2。
运用场景:在需要对字符串进行排序或查找时。
strncmp(s1, s2, n): 比较 s1 和 s2 的前 n 个字符。
运用场景:当只需要比较字符串的前几个字符时。
5.字符串查找
strstr(s1, s2): 在字符串 s1 中查找子字符串 s2。
运用场景:当需要在字符串中查找特定的子串时。
strchr(s, c): 在字符串 s 中查找字符 c 的第一次出现。
运用场景:查找特定字符的位置。
strrchr(s, c): 在字符串 s 中查找字符 c 的最后一次出现。
运用场景:从字符串末尾开始查找特定字符的位置。
字符串其他操作
strspn(s1, s2): 返回 s1 中由 s2 中任意字符组成的初始子串的长度。
运用场景:用于查找字符串中的前导字符。
strcspn(s1, s2): 返回 s1 中由不在 s2 中的任意字符组成的初始子串的长度。
运用场景:用于查找字符串中第一个不符合条件的字符的位置。
strpbrk(s1, s2): 在字符串 s1 中查找 s2 中任何一个字符的第一次出现。
运用场景:查找字符串中任意一个字符的位置。
strtok(s1, s2): 将字符串 s1 拆分为一系列由 s2 中任意字符分隔的标记。
运用场景:用于字符串分割,比如解析命令行参数。
#include <stdio.h>
#include <string.h>
int main() {
char source[] = "Hello, World!";
char dest[20]; // 确保这个数组足够大,可以容纳 source 字符串
// 复制字符串
strcpy(dest, source);
// 输出复制的字符串及其长度
printf("Copied string: %s\n", dest);
printf("Length of copied string: %lu\n", strlen(dest));
return 0;
}
p.s.在C语言编程中,以下是一些可能会使用 strcpy
函数的具体题目:
- 字符串复制:
编写一个程序,将用户输入的字符串复制到一个新的字符串变量中。 - 字符串处理函数:
实现一个函数,该函数接收两个字符串参数,并返回第一个字符串的副本,但所有小写字母都转换为大写。 - 字符串拼接:
编写一个程序,将两个字符串拼接成一个新字符串,并确保结果字符串不会溢出。 - 数据结构实现:
在实现链表、树或其他数据结构时,可能需要复制字符串作为节点数据。
题目1:字符串复制
编写一个C程序,它要求用户输入一个字符串,然后使用 strcpy 函数将这个字符串复制到一个新的字符串变量中,并打印出复制的字符串。
#include<stdio.h>
#include<string.h>
int main(){
char s[20] = "hellow,tom";
char new[20];
strcpy(new,s);
printf("%s",new);
return 0;
}
题目2:字符串处理函数
编写一个函数 toUpperCase,它接收一个字符串并返回一个新的字符串,其中所有小写字母都被转换为大写。
#include<stdio.h>
#include<string.h>
void toUpperCase(char s[20]){//void!!!
//char function,返回的是一个字符,不可以返回字符数组
int i;
i = 0;
while(s[i] != '\0'){
if(s[i] >= 97 && s[i] <= 122){
s[i] = s[i] - 32;
}
i ++;//写while不要忘记i ++!!!!
}
}
int main(){
char s[20] = "hellow,tom";
// toUpperCase(s[20]);//错误的,代表传递第21个子级
toUpperCase(s);//传递s就可以了
printf("%s",s);
return 0;
}
//在 ASCII 码(美国信息交换标准代码)中,大写字母A - Z的编码值是从 65 到 90,小写字母a - z的编码值是从 97 到 122。
//可以发现,同一个字母的大写和小写形式的 ASCII 码值相差 32
在这些题目中,strcpy 用于创建字符串的副本,以便在不改变原始字符串的情况下进行操作。在使用 strcpy 时,务必确保目标缓冲区足够大,以避免缓冲区溢出。
在C语言中,避免参数传递错误可以从以下几个方面入手:
1. 理解函数参数的类型和要求
- 仔细查看函数定义:在调用函数之前,务必仔细查看函数的声明或定义,明确每个参数的类型、是传值还是传指针(引用)以及期望的参数值范围等信息。
例如,如果函数定义为void func(int *ptr)
,那就清楚它需要传递一个指向int
类型的指针。 - 区分值传递和指针(引用)传递:
- 值传递:对于基本数据类型(如
int
、float
、char
等)的参数,默认是值传递。这意味着函数内部会创建一个参数的副本进行操作,不会影响原始变量的值。例如:
void increment(int num) { num++; } int main() { int a = 5; increment(a); printf("%d\n", a); // 输出仍然是5,因为函数内部操作的是副本 return 0; }
- 指针(引用)传递:如果要在函数内部修改原始变量的值,或者传递大型数据结构(如数组、结构体等)以避免复制开销,通常使用指针(或引用,C++概念,C语言中类似通过指针实现)传递。例如:
void increment_ptr(int *num_ptr) { (*num_ptr)++; } int main() { int a = 5; increment_ptr(&a); printf("%d\n", a); // 输出是6,因为函数内部通过指针修改了原始变量的值 return 0; }
- 值传递:对于基本数据类型(如
- 注意函数参数的求值顺序(如果不确定):在某些复杂的表达式作为函数参数时,要注意参数的求值顺序可能因编译器而异。例如,在
func(a++, b++)
这样的调用中,不同编译器可能先计算a++
还是b++
,为了避免不确定性,尽量避免在一个函数调用中使用多个有副作用的表达式作为参数。
2. 正确传递数组和指针参数
- 数组作为参数传递:
- 当数组作为函数参数时,数组名会退化为指向数组首元素的指针。所以在传递数组时,应该传递数组名。例如:
void print_array(int *arr, int size) { for (int i = 0; i < size; i++) { printf("%d ", arr[i]); } } int main() { int a[] = {1, 2, 3, 4, 5}; print_array(a, sizeof(a)/sizeof(a[0])); return 0; }
- 要注意在函数内部不能再使用
sizeof
来获取数组的真实大小(因为此时它只是一个指针),应该将数组大小作为另一个参数传递给函数。
- 指针参数的合法性检查:
- 在传递指针之前,确保指针是有效的。如果指针可能为
NULL
,在函数内部应该先进行检查。例如:
void process_data(int *data) { if (data == NULL) { printf("无效的指针\n"); return; } // 正常处理数据 }
- 同时,要确保指针指向的内存区域是合法的、已经正确分配和初始化的。比如,不能传递一个指向已经释放的内存区域或者未初始化的指针给函数,除非函数内部有专门的机制来处理这种情况。
- 在传递指针之前,确保指针是有效的。如果指针可能为
3. 结构体和其他复杂数据类型作为参数
- 结构体传递方式选择:
- 可以选择传递结构体本身(值传递),但这样会产生复制开销。对于大型结构体,这可能会影响性能。例如:
struct Point { int x; int y; }; void modify_point(struct Point p) { p.x++; p.y++; } int main() { struct Point p = {1, 2}; modify_point(p); printf("(%d, %d)\n", p.x, p.y); // 输出仍然是(1, 2),因为是值传递 return 0; }
- 更常用的是传递结构体指针,这样可以在函数内部修改结构体的成员。例如:
void modify_point_ptr(struct Point *p) { p->x++; p->y++; } int main() { struct Point p = {1, 2}; modify_point_ptr(&p); printf("(%d, %d)\n", p.x, p.y); // 输出是(2, 3),因为通过指针修改了结构体成员 return 0; }
- 对于包含指针成员的结构体:如果结构体包含指针成员,要确保指针成员指向的内存是有效的。在传递这样的结构体时,可能需要在函数内部对指针成员指向的内存区域进行额外的操作和检查,比如动态内存分配或释放操作。
传递数组名和元素个数(最常见的指针方式)!!!