C/C++预编译指令
在C/C++(以及许多其他支持预处理器的编程语言,如Objective-C、D等)中,预处理指令是用于在编译之前对源代码进行处理的特殊指令。这些指令不是C/C++语言的一部分,而是由预处理器(preprocessor)执行的。预处理器的主要任务包括宏定义、条件编译、文件包含等。下面是一些常见的预处理指令:
-
#define:用于定义宏。它可以定义常量、宏函数(宏展开)等。预处理器会在编译之前将所有的宏名称替换为它们相应的值或代码段。
#define MAX 100 #define SQUARE(x) ((x) * (x))
-
#include:用于包含(插入)另一个文件的内容到当前文件中。这常用于包含标准库头文件或用户自定义的头文件。
#include <stdio.h> #include "myheader.h"
-
#ifdef、#ifndef、#endif:这些指令用于条件编译。
#ifdef
检查某个宏是否已定义,如果已定义,则编译其后的代码直到遇到#endif
。#ifndef
则是检查某个宏是否未定义,如果未定义,则编译其后的代码直到遇到#endif
。#define DEBUG #ifdef DEBUG printf("Debugging\n"); #endif
-
#if、#elif、#else、#endif:这些指令提供了更复杂的条件编译能力。
#if
后跟一个整数常量表达式(通常是宏的替换结果),如果表达式非零,则编译其后的代码直到遇到#endif
。#elif
和#else
提供了额外的条件分支。#define LEVEL 2 #if LEVEL > 0 printf("Level is positive.\n"); #elif LEVEL < 0 printf("Level is negative.\n"); #else printf("Level is zero.\n"); #endif
-
#undef:用于取消宏的定义。
#define PI 3.14 // 使用PI #undef PI // 现在PI未定义
-
#pragma:这是一个特殊的预处理指令,用于向编译器提供额外的指令,这些指令不是C/C++标准的一部分,而是由特定的编译器支持。例如,
#pragma once
用于防止头文件被重复包含,但这并不是一个标准的预处理指令,而是由一些编译器提供的。#pragma once // 头文件内容
-
#error 和 #warning:
#error
用于生成编译时错误,而#warning
用于生成编译时警告。这些指令通常用于在特定条件不满足时通知开发者。#if !defined(MY_MACRO) #error "MY_MACRO is not defined!" #endif
这些预处理指令为C/C++(及支持预处理器的其他语言)提供了强大的宏定义和条件编译能力,使得代码更加灵活和可复用。
编译时候为什么不带.h文件
在编译过程中,.h
(头文件)文件本身通常不会被直接编译成可执行文件或库文件。.h
文件主要用于声明函数、变量、类型定义(如结构体、联合体、枚举等)、宏定义等,它们为编译器提供了关于程序中其他部分(如.c
文件)如何交互的信息。然而,这些声明本身并不包含实现代码,因此不需要(也不能)被编译成机器码。
那么,为什么编译时看起来“不带.h文件”呢?这主要是因为:
- 预处理阶段:在编译过程的早期阶段(即预处理阶段),编译器会处理所有的
#include
指令,将.h
文件中的内容直接插入到包含它的.c
文件中。这意味着,当编译器开始处理.c
文件时,它实际上已经“看到”了所有相关的.h
文件中的内容。因此,从编译器的角度来看,它并不直接处理.h
文件,而是处理已经包含了.h
文件内容的.c
文件。 - 编译和链接:在预处理之后,编译器会编译
.c
文件(现在包含了所有相关的.h
文件的内容),生成目标文件(如.o
或.obj
文件)。这些目标文件包含了程序的实际机器码。然后,链接器会将这些目标文件以及任何必要的库文件链接起来,生成最终的可执行文件或库文件。 - 头文件的重要性:尽管
.h
文件本身不被编译,但它们在软件开发中扮演着至关重要的角色。它们使得代码更加模块化,提高了代码的可读性和可维护性。通过声明接口而不是实现细节,.h
文件允许开发者在不同的源文件之间共享代码,而无需担心实现的具体细节。 - 编译时看起来不带
.h
文件,是因为.h
文件的内容在预处理阶段已经被插入到了.c
文件中,而编译器实际上是在处理这些已经包含了.h
文件内容的.c
文件。
C/C++头文件引入时的细节
在C和C++中,使用预处理指令#include
来包含(或插入)头文件时,可以使用两种不同的符号:双引号(" ")和尖括号(< >)。
区别
- 搜索路径
- 使用尖括号(< >)时,编译器会在系统或标准库指定的路径中查找头文件。这些路径通常是编译器安装时预设的,包含了标准库的头文件。
- 使用双引号(" ")时,编译器会首先在包含该指令的源文件所在的目录(即当前目录)中查找头文件,如果没有找到,再按照编译器配置的其他路径(可能包括标准库路径)进行查找。
- 用途
- 尖括号(< >)通常用于包含标准库头文件,因为这些头文件通常位于系统指定的路径中。
- 双引号(" ")则更多地用于包含用户自定义的头文件,因为这些头文件通常位于当前项目的目录下。
- 灵活性
- 使用双引号(" ")时,如果头文件位于非标准路径下,可以通过修改编译器的包含路径(例如,使用编译器的
-I
选项)来指定额外的搜索路径。 - 尖括号(< >)则没有这种灵活性,它总是按照预定义的路径进行查找。
- 使用双引号(" ")时,如果头文件位于非标准路径下,可以通过修改编译器的包含路径(例如,使用编译器的
联系
- 无论是使用尖括号(< >)还是双引号(" "),
#include
指令的目的都是为了在编译时将指定的头文件内容插入到当前源文件中,以便编译器能够找到并使用其中声明的函数、类型、宏等。 - 在某些情况下,如果当前目录下存在与标准库头文件同名的用户自定义头文件,并且使用了双引号(" ")进行包含,编译器会优先使用当前目录下的文件,这可能会导致意外的编译错误或行为。因此,在包含标准库头文件时,建议使用尖括号(< >)以避免潜在的冲突。
编程习惯
- 一般来说,良好的编程习惯是使用尖括号(< >)来包含标准库头文件,使用双引号(" ")来包含用户自定义的头文件。这样做有助于区分不同类型的头文件,并提高代码的可读性和可维护性。
- 此外,为了避免头文件被重复包含,通常会在每个头文件的开头和结尾使用预处理指令(如
#ifndef
、#define
和#endif
)来定义一个唯一的宏,从而确保头文件的内容只会被包含一次。
C/C++中的" "和’ '的区别
在C语言和C++中,双引号""
和单引号''
的使用有着本质的区别,主要体现在它们所表示的数据类型和存储方式上。
-
双引号
""
:- 用于表示一个字符串(String)。
- 字符串是由零个或多个字符组成的序列,并且以空字符(
\0
)作为结束标志。 - 在内存中,字符串以字符数组的形式存储,但不需要显式地包含结束符
\0
,因为编译器会自动在字符串的末尾添加一个\0
。 - 字符串可以通过
char
数组或std::string
(在C++中)来处理。 - 示例:
char str[] = "Hello, World!";
或者在C++中std::string str = "Hello, World!";
-
单引号
''
:- 用于表示单个字符(Character)。
- 在内存中,字符直接以其ASCII码(或其他字符编码,如UTF-8)的形式存储。
- 字符类型在C和C++中通常是
char
类型。 - 示例:
char ch = 'A';
注意事项:
- 如果在单引号内放入多于一个字符(除了转义字符如
\n
、\t
等),这会导致编译错误。 - 字符串字面量(即双引号内的内容)在内存中占用的是连续的内存空间,以存储字符串中的每个字符以及结尾的空字符
\0
。而字符字面量(即单引号内的内容)只占用一个字节(在大多数系统中),用于存储该字符的ASCII码或其他字符编码值。 - 在C++中,除了使用
char
数组表示字符串外,还可以使用std::string
类,它提供了更丰富的字符串处理功能,并且会自动管理内存,避免了手动处理字符串时可能遇到的一些常见问题(如缓冲区溢出)。
C语言各种类型的输出
整数类型:
%d
:以十进制形式输出整数。%x
:以十六进制形式输出整数。%o
:以八进制形式输出整数。%u
:以无符号十进制形式输出整数。
int num = 10;
printf("Decimal: %d\\n", num);
printf("Hexadecimal: %x\\n", num);
printf("Octal: %o\\n", num);
浮点数类型:
%f
:以小数形式输出浮点数。%e
:以指数形式输出浮点数。%g
:以%f或%e中较短的形式输出。
float num = 3.14;
printf("Float: %f\\n", num);
printf("Exponential: %e\\n", num);
字符类型:
%c
:输出单个字符。
char ch = 'A';
printf("Character: %c\\n", ch);
字符串类型:
%s
:输出字符串。
char str[] = "Hello, World!";
printf("String: %s\\n", str);
指针类型:
%p
:输出指针地址。
int *ptr;
printf("Pointer Address: %p\\n", (void*)&ptr);
各种数据类型所占空间大小
- 基本整型数据类型:
char
: 1 字节short
: 2 字节int
: 4 字节long
: 4或8 字节(取决于系统)long long
: 8 字节
- 浮点型数据类型:
float
: 4 字节double
: 8 字节long double
: 10 字节或更多(取决于系统)32为系统 12 64位系统 16
- 指针类型:
- 所有指针类型在 32 位系统上通常占用 4 字节,在 64 位系统上通常占用 8 字节。
- 枚举类型:
- 枚举类型的大小通常和
int
类型相同,即 4 字节。
- 枚举类型的大小通常和
- void 类型:
void
类型没有固定大小。
需要注意的是,以上数据类型的大小是一般情况下的情况,实际情况可能会因编译器、操作系统和架构的不同而有所变化。在实际开发中,可以使用 sizeof
运算符来获取特定数据类型在当前系统上的大小。例如,sizeof(int)
可以返回 int
类型在当前系统上的字节数。
关键字和基本类型
1.关键字
①基本数据类型
void :声明函数无返回值或无参数,声明无类型指针,显式丢弃运算结果
char :字符型类型数据,属于整型数据的一种
int :整型数据,通常为编译器指定的机器字长
float :单精度浮点型数据,属于浮点数据的一种
double :双精度浮点型数据,属于浮点数据的一种
②类型修饰关键字
short :修饰int,短整型数据,可省略被修饰的int。
long :修饰int,长整形数据,可省略被修饰的int。
signed :修饰整型数据,有符号数据类型
unsigned :修饰整型数据,无符号数据类型
③复杂类型关键字
struct :结构体声明
union :共用体声明
enum :枚举声明
typedef :声明类型别名
sizeof :得到特定类型或特定类型变量的大小
④存储级别关键字
auto :指定为自动变量,由编译器自动分配及释放。通常在栈上分配
static :指定为静态变量,分配在静态变量区,修饰函数时,指定函数作用域为文件内部
register :指定为寄存器变量,建议编译器将变量存储到寄存器中使用,也可以修饰函数形参,建议编译器通过寄存器而不是堆栈传递参数
extern :指定对应变量为外部变量,即标示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义。
const :与volatile合称“cv特性”,指定变量不可被当前线程/进程改变(但有可能被系统或其他线程/进程改变)
volatile :与const合称“cv特性”,指定变量的值有可能会被系统或其他进程/线程改变,强制编译器每次从内存中取得该变量的值
2.基本类型
1.32位编译器:
char :1个字节
char*(即指针变量): 4个字节(32位的寻址空间是2^32, 即32个bit,也就是4个字节。同理64位编译器)
short int : 2个字节
int: 4个字节
unsigned long: 4个字节
unsigned int : 4个字节
float: 4个字节
long: 4个字节
long long: 8个字节
double: 8个字节
2.64位编译器
char :1个字节
char*(即指针变量): 8个字节
short int : 2个字节
int: 4个字节
unsigned int : 4个字节
float: 4个字节
long: 8个字节
long long: 8个字节
double: 8个字节
通用类型指针void
在C和C++中,通用类型指针void*
扮演着重要的角色,它提供了一种方式来处理未知或不同类型的数据。尽管C和C++在类型系统和语言特性上有所不同,但void*
在这两种语言中的作用大致相同,不过在具体的使用场景和最佳实践上可能会有所差异。
通用性
void*
指针是一种特殊类型的指针,它可以指向任何类型的数据。然而,由于void
类型本身不表示任何具体的数据类型,因此不能直接对void*
指针进行算术运算(如递增或递减)或解引用(除非先将其转换为具体类型的指针)。这种通用性使得void*
成为在需要处理不同类型数据但又不想(或不能)使用泛型或模板时的理想选择。
在C中的作用
在C中,void*
指针被广泛用于以下场景:
-
内存管理:
malloc
、calloc
和realloc
等函数返回void*
类型的指针,因为它们分配的内存可以存储任何类型的数据。调用者需要将void*
指针转换为适当的类型指针才能访问或修改存储的数据。 -
泛型数据结构:在实现如链表、树等泛型数据结构时,节点通常使用
void*
指针来存储指向不同类型数据的指针。这样,同一个数据结构就可以用来存储整数、浮点数、结构体等多种类型的数据。 -
回调函数:在C中,回调函数经常需要处理未知类型的数据,这时
void*
指针就可以作为传递数据的手段。回调函数内部会将void*
指针转换为适当的类型以进行进一步的处理。
在C++中的作用
在C++中,虽然有了模板和类型安全的特性,但void*
指针仍然有其用武之地:
-
与C代码交互:当C++代码需要与C代码交互时(例如,使用C库或编写需要被C代码调用的接口),
void*
指针是必需的,因为C语言不支持模板和C++的类型安全特性。 -
类型擦除:在某些情况下,C++代码可能需要实现类型擦除,即隐藏对象的实际类型。虽然C++通常使用虚函数和基类指针来实现多态和类型擦除,但在某些特定场景下(如需要完全的类型擦除或优化内存使用),
void*
指针结合其他机制(如函数指针或类型标识)也可以用来实现类型擦除。 -
低级内存操作:在进行低级内存操作时(如直接操作内存缓冲区、实现自定义的内存分配器等),
void*
指针仍然是必要的。
注意事项
-
类型转换:将
void*
指针转换为其他类型的指针时,必须确保这种转换是安全的。错误的类型转换可能导致未定义行为,如访问违规或数据损坏。 -
解引用:在将
void*
指针转换为具体类型的指针之前,不能直接解引用void*
指针来访问其指向的数据。 -
模板与泛型:在C++中,模板提供了一种更类型安全、更灵活的方式来处理不同类型的数据。因此,在C++代码中,应优先考虑使用模板而不是
void*
指针,除非有特定的理由需要使用void*
。
总的来说,void*
指针在C和C++中都扮演着重要的角色,但它们的使用场景和最佳实践在两种语言中可能有所不同。在C中,void*
指针是处理不同类型数据的常用手段;而在C++中,虽然模板和类型安全特性更加受青睐,但void*
指针在特定场景下仍然有其用武之地。
new,delete与malloc,free
1.malloc和free
malloc
和 free
是 C 语言标准库中的函数,用于在堆(heap)上动态地分配和释放内存。这些函数定义在 <stdlib.h>
头文件中。
malloc
函数用于在堆上分配指定大小的内存块,并返回指向该内存块的指针。如果内存分配成功,malloc
返回一个非空指针;如果分配失败(例如,由于内存不足),则返回 NULL
。
函数的原型如下:
void *malloc(size_t size);
其中,size
参数表示要分配的字节数。由于 malloc
返回一个 void
指针,因此通常需要将其转换为适当的类型指针来存储返回的内存地址。
例如:
int *ptr = (int *)malloc(sizeof(int) * 10);
if (ptr == NULL) {
// 内存分配失败,处理错误
} else {
// 使用 ptr 指向的内存块
}
free
函数则用于释放之前通过 malloc
(或其他相关函数如 calloc
或 realloc
)分配的内存块。调用 free
后,指针本身并不会被自动设置为 NULL
,因此通常建议显式地将指针设置为 NULL
,以避免悬挂指针(dangling pointer)的问题。
函数的原型如下:
void free(void *ptr);
其中,ptr
是指向要释放的内存块的指针。传递 NULL
给 free
是安全的,并且不会有任何效果。
例如:
// 假设 ptr 是一个之前通过 malloc 分配的内存块的指针
free(ptr);
ptr = NULL; // 避免悬挂指针
正确使用 malloc
和 free
是编写健壮的 C 语言程序的关键部分,因为它们涉及到内存管理,而内存管理是编程中一个常见且容易出错的领域。不正确的内存管理可能导致内存泄漏、程序崩溃或其他未定义行为。
2.new和delete
new
和 delete
运算符在 C++ 中确实与构造函数和析构函数的调用密切相关。
当你使用 new
运算符来动态地分配内存并创建一个对象时,该对象的构造函数会被自动调用。这个过程包括两个步骤:首先,new
在堆上分配足够的内存来存储对象;然后,它调用对象的构造函数来初始化这块内存。
例如:
class MyClass {
public:
MyClass() {
// 构造函数体
}
~MyClass() {
// 析构函数体
}
};
MyClass* obj = new MyClass(); // 这里调用了 MyClass 的构造函数
在上面的代码中,new MyClass()
会做两件事情:
- 在堆上分配足够的内存来存储一个
MyClass
对象。 - 调用
MyClass
的构造函数来初始化这块内存。
另一方面,当你使用 delete
运算符来释放一个通过 new
创建的对象时,该对象的析构函数会被自动调用。析构函数用于执行任何必要的清理工作,比如释放对象所拥有的资源。
例如:
delete obj; // 这里调用了 MyClass 的析构函数
在上面的代码中,delete obj;
会做两件事情:
- 调用
obj
所指向的MyClass
对象的析构函数。 - 释放
obj
所指向的内存。
因此,通过 new
和 delete
运算符管理对象的生命周期时,会确保对象的构造函数和析构函数得到正确的调用。这对于资源管理和避免内存泄漏是非常重要的。
new
和 delete
是 C++ 中的运算符,用于在动态内存分配中创建和销毁对象。这些运算符是 C++ 对 C 语言中 malloc
和 free
的扩展,提供了更高级别的内存管理功能,包括自动调用构造函数和析构函数。
3.区别和联系
malloc和new都是在动态内存分配时使用的工具,但它们有一些重要的区别。
malloc是C语言中的函数,全称是memory allocation,中文叫动态内存分配。它用于申请一块连续的指定大小的内存块区域,并以void*类型返回分配的内存区域地址。如果分配成功,则返回指向被分配内存的指针;否则,返回空指针NULL。在使用malloc开辟空间时,使用完成后一定要释放空间,否则会造成内存泄漏。
而new是C++特有的运算符,用于动态地从堆中分配内存。其基本用法是:new 类型,这能在内存中创建一个指定类型的新元素,同时返回一个指向它的指针。new在创建并初始化对象时比malloc更便捷,因为它会自动调用构造函数并返回指向对象的指针,而malloc只是返回分配空间的地址,不会调用构造函数。此外,如果内存分配失败,new能够抛出bad_alloc异常,而malloc只会返回null指针。
在C++中使用new
操作符而不是malloc
函数的主要原因是new
操作符是C++中的一部分,它提供了更多的功能和安全性,而且更符合面向对象的编程风格。以下是一些使用new
而不是malloc
的原因:
- 类型安全性:
new
操作符会自动计算所需的内存空间,并返回正确类型的指针。这可以避免因类型不匹配而导致的错误。 - 构造函数调用: 如果使用
new
操作符来分配内存,可以自动调用对象的构造函数,确保对象被正确初始化。而malloc
只是分配内存空间,不会调用构造函数。 - 内存泄漏检测: 使用
new
操作符分配的内存,当对象超出作用域时,会自动调用析构函数释放内存。这有助于避免内存泄漏问题。 - 异常处理:
new
操作符在分配内存时会抛出std::bad_alloc
异常,可以通过异常处理机制来处理内存分配失败的情况。而malloc
在分配内存失败时只会返回NULL
,需要手动检查并处理。 - 数组分配:
new
操作符可以方便地分配数组,并在释放内存时正确调用数组元素的析构函数。而malloc
则需要手动计算数组元素的大小和释放顺序。
总的来说,malloc和new在语法、类型处理、异常处理等方面都有所不同。malloc更适用于纯C语言中的内存分配,而new更适用于C++中创建和初始化对象。
Strlen和Sizeof的区别
在C和C++中,sizeof
和strlen
是两个用于计算大小的工具,但它们之间存在显著的区别。以下是对这两个工具区别的详细解析:
1. 定义与类型
- sizeof:是C/C++中的一个操作符(Operator),用于在编译时计算变量或类型所占的内存大小(以字节为单位)。它不是函数,因此不需要包含任何头文件即可使用。
- strlen:是C/C++中的一个函数(Function),定义在
<string.h>
(C语言)或<cstring>
(C++)头文件中,用于计算以空字符(‘\0’)结尾的字符串的长度(不包括终止的空字符)。
2. 参数类型与用法
- sizeof:
- 可以接受任何类型的参数,包括数组、对象、指针、函数名等。
- 当用于数组时,它返回整个数组所占的内存大小(以字节为单位),包括所有元素和可能的填充字节。
- 当用于指针时,它返回指针本身所占的内存大小,这通常与平台的指针大小有关(如32位系统下通常是4字节,64位系统下通常是8字节)。
- strlen:
- 只能接受
char*
类型的参数,即指向字符数组(字符串)的指针。 - 它遍历字符串直到遇到第一个空字符(‘\0’),并返回从字符串开始到该空字符之前的字符数。
- 只能接受
3. 计算时间与结果
- sizeof:
- 在编译时计算其操作数的大小,因此其结果是编译时就已知的,不会增加程序的运行时间开销。
- 其返回值类型为
size_t
,是一个无符号整数类型,能够表示对象可能的最大大小。
- strlen:
- 在运行时计算字符串的长度,因此会增加程序的运行时间开销,特别是在处理长字符串时。
- 其返回值类型同样是
size_t
,表示计算得到的字符串长度(不包括终止的空字符)。
4. 使用场景与注意事项
- sizeof:
- 适用于需要知道数据或类型在内存中占用多少字节的场景,如内存分配、数组遍历等。
- 特别地,当需要定义数组的大小时,可以使用
sizeof
来获取元素的数量(通过sizeof(数组名) / sizeof(数组名[0])
)。
- strlen:
- 仅适用于处理以空字符结尾的字符串,不能用于计算非字符串类型的数据长度。
- 使用
strlen
时需要确保传入的指针确实指向了一个以空字符结尾的字符串,否则可能会导致未定义行为(如访问违规)。
5. 示例对比
假设有以下定义:
char str[] = "Hello, world!";
char *ptr = str;
-
使用
sizeof
:sizeof(str)
:返回整个数组str
所占的内存大小,包括字符串的结尾空字符,假设是char
类型,则结果为14
(因为字符串长度为12
,加上一个空字符和一个可能的字节对齐填充)。sizeof(ptr)
:返回指针ptr
本身所占的内存大小,这通常与平台的指针大小有关,如4
字节或8
字节。
-
使用
strlen
:strlen(str)
:返回字符串"Hello, world!"
的长度,不包括结尾的空字符,结果为12
。strlen(ptr)
:与strlen(str)
相同,因为ptr
指向了str
,所以结果也是12
。
结论
sizeof
用于计算内存大小,而strlen
用于计算字符串长度。理解和正确使用这两个工具对于编写高效、安全的代码至关重要。
静态关键字static在C和C++的区别
在C语言和C++中,static
关键字的基本用途是相似的,但它们在某些上下文中的具体行为或用法可能有所不同。static
关键字通常用于以下目的:
-
局部静态变量:在函数内部定义的静态变量,它在程序的生命周期内只被初始化一次,并在函数调用之间保持其值。这个用法在C和C++中是相同的。
-
全局静态变量:在文件作用域中声明的静态变量,这种变量只能在其定义的文件内部访问。这有助于隐藏变量,避免命名冲突,并在不同文件之间提供隔离。这个用法在C和C++中也是相同的。
然而,在C++中,static
关键字还有一些额外的用途,特别是在类(class)和成员函数(methods)的上下文中,这些用法在C语言中是不存在的:
-
静态成员函数:在C++中,可以声明类的成员函数为静态的。静态成员函数属于类本身,而不是类的某个对象。因此,它们不能访问类的非静态成员(包括非静态成员变量和非静态成员函数),除非通过类的某个对象来访问。静态成员函数可以用类名直接调用,而无需创建类的实例。
-
静态成员变量:类似于静态成员函数,静态成员变量也是属于类本身的,而不是类的某个特定对象。静态成员变量在类的所有对象之间共享,并且必须在类的外部进行定义和初始化(除非它们被声明为内联的)。
-
静态块(仅限C++):C++中不存在直接称为“静态块”的语法结构,但可以通过静态局部变量和静态成员函数的初始化块(在类定义中)来模拟静态初始化代码块的效果。然而,这与Java中的静态块(static blocks)在概念上有相似之处,但实现方式不同。
总的来说,C和C++中static
关键字的基本用途相似,但在C++中,由于类的引入,static
关键字被赋予了更多与类和成员相关的特定用途。
const关键字
const
关键字在 C 和 C++ 中是一个非常有用的特性,它用于指定一个变量的值在初始化之后不能被修改。这个特性在多种编程场景中都非常有用,比如提高代码的可读性、保护数据不被意外修改、优化编译器对代码的处理等。下面详细解释 const
在 C 和 C++ 中的用法和区别(尽管在大多数用法上两者是相似的)。
基本用法
-
修饰变量:
const int a = 10; // a 是一个常量,其值不能被修改
-
修饰指针:
- 指向常量的指针(指针指向的内容不能通过该指针修改):
const int *ptr = &a; // ptr 是一个指向常量的指针,不能通过 ptr 修改 a 的值
- 常量指针(指针本身的值不能修改,但指针指向的内容可以修改):
int b = 20; int * const cptr = &b; // cptr 是一个常量指针,cptr 的值(即它所指向的地址)不能被修改
- 指向常量的常量指针(两者都不能修改):
const int * const cptr2 = &a; // cptr2 指向一个常量,且 cptr2 本身的值也不能被修改
- 指向常量的指针(指针指向的内容不能通过该指针修改):
在 C 和 C++ 中的区别
尽管 const
在 C 和 C++ 中的基本用法相似,但在一些高级特性和用途上存在一些差异:
-
默认函数参数:
- 在 C 中,函数参数不能使用
const
修饰的变量作为默认值。 - 在 C++ 中,可以使用
const
修饰的变量作为函数的默认参数。
- 在 C 中,函数参数不能使用
-
模板和泛型编程:
- C++ 支持模板和泛型编程,其中
const
关键字的使用更加灵活和复杂。例如,在模板函数中,可以通过模板参数是否为const
来特化模板,实现不同版本的函数。
- C++ 支持模板和泛型编程,其中
-
类的成员函数:
- 在 C++ 中,
const
可以用来修饰类的成员函数,表示该函数不会修改类的任何成员变量(除了那些被声明为mutable
的成员变量)。 - 这对于保持类的封装性和数据一致性非常重要。
- 在 C++ 中,
-
隐式类型转换:
- 在 C++ 中,编译器对
const
的处理更加严格,可能会阻止一些隐式类型转换,特别是当涉及到const
修饰的指针或引用时。
- 在 C++ 中,编译器对
-
字符串字面量:
- 在 C 和 C++ 中,字符串字面量默认都是
const char[]
类型的,意味着你不能通过指针修改字符串字面量的内容。
- 在 C 和 C++ 中,字符串字面量默认都是
总结
const
关键字在 C 和 C++ 中都是非常重要的,它提供了对变量和指针的额外保护,使得代码更加健壮和安全。在 C++ 中,const
的使用更加广泛和复杂,支持了更多的编程范式和特性,如模板、泛型编程和类的成员函数等。了解和熟练使用 const
关键字对于编写高质量的 C 和 C++ 代码至关重要。
&在C和C++的区别
在C和C++中,符号&
的用法存在显著的差异,主要体现在以下几个方面:
1. 作为取地址运算符
在C和C++中,当&
符号位于一个已存在的变量前面时,它都作为取地址运算符,用于获取该变量的内存地址。这是&
在两种语言中共同的基本用法。
示例:
int a = 10;
int *ptr = &a; // 在C和C++中,&a都表示取变量a的地址
2. 作为引用(C++特有)
在C++中,&
被引入了一个新的用途,即作为引用类型的声明符。引用类型是对另一个变量的别名,对引用的操作实际上是对被引用变量的操作,且引用一旦绑定到某个变量后,就不能再改变为引用另一个变量。这是C++相对于C的一个重要扩展。
示例:
int a = 10;
int &ref = a; // ref是a的引用,对ref的操作等价于对a的操作
ref = 20; // 此时a的值也变为20
3. 函数参数传递
- 在C中:函数参数通常通过指针来传递,以便在函数内部修改外部变量的值。此时,
&
用于获取变量的地址,并将其传递给函数。
示例:
void func(int *p) {
*p = 20; // 修改指针p指向的值
}
int a = 10;
func(&a); // 传递a的地址给func函数
- 在C++中:除了使用指针外,C++还允许通过引用来传递函数参数。此时,
&
用于声明引用类型的参数,这样函数可以直接操作传入的变量本身,而不需要通过指针解引用。
示例:
void func(int &p) {
p = 20; // 直接修改p(即传入的变量)的值
}
int a = 10;
func(a); // 直接传递变量a,而不是其地址
4. 返回值
-
在C中:函数不能返回局部变量的地址,因为局部变量的生命周期在函数返回后就结束了。如果需要返回数据,通常通过返回值或指针参数来实现。
-
在C++中:除了使用指针返回数据外,C++还允许通过引用来返回局部变量的引用(但仅限于返回对静态局部变量或函数外部变量的引用,因为非静态局部变量的生命周期在函数返回后就结束了)。然而,更常见的是返回对象的引用,特别是在实现运算符重载或返回大型对象时,以避免不必要的拷贝。
总结
在C中,&
主要作为取地址运算符使用;而在C++中,&
除了作为取地址运算符外,还引入了引用类型的新概念,极大地丰富了C++的语法和表达能力。这些差异使得C++在函数参数传递、返回值以及类与对象的设计等方面具有更高的灵活性和效率。
x=x+1, x+=1, x++ 哪个效率更高?
x=x+1最低,因为它的执行过程如下:
(1)读取右x的地址。
(2)x+1.
(3)读取左x的地址。
(4)将右值传给左边的x(编译器并不认为左右x的地址相同)。
x+=1其次,其执行过程如下:
(1)读取右x的地址。
(2)x+1.
(3)将得到的值传给x(因为x的地址已经读出)。
x++效率最高,其执行过程如下:
(1)读取右x的地址。
(2)x自增1。
答案:x++效率最高。
浮点数不能用==?原因是什么
浮点数不能直接使用 ==
操作符进行比较的主要原因是浮点数在计算机中的表示方式导致的精度问题。
在计算机中,浮点数通常遵循IEEE 754标准来表示,这种表示方式包括一个符号位、一个指数部分和一个尾数(或称为有效数字)部分。然而,由于浮点数的表示是离散的,并且受到存储位数的限制,许多小数(特别是那些无法精确转换为二进制形式的小数)在计算机中只能被近似表示。
当你对浮点数进行算术运算时(如加法、乘法等),由于这种近似的表示,结果可能会产生微小的舍入误差。即使两个数学上不相等的数,在计算机中经过运算后也可能因为舍入误差而得到相同的浮点表示。相反,数学上相等的两个数,在计算机中可能因为舍入误差而得到稍微不同的浮点表示。
因此,直接使用 ==
操作符来比较两个浮点数是否相等,很可能会因为微小的舍入误差而导致意外的结果(即使两个数在数学上应该是相等的,比较结果也可能是 false
)。
为了解决这个问题,通常会采用一种称为“容错比较”的方法,即定义一个小的正数(称为epsilon或容差)作为误差范围,然后比较两个浮点数的差的绝对值是否小于这个epsilon值。如果是,则认为这两个数在可接受的误差范围内是相等的。这种方法在处理浮点数时更为可靠和准确。
float的比double精度低,两者互相比较,常发生精度带来的相等性判断问题
float型数据提供7位有效数字
double型数据提供16位有效数字
#define EPS 1E-5
float f = 123.456f; // 注意添加f后缀以明确是float类型
if (fabs(f - 123.456f) < EPS) {
printf("float numbers are equal within the given epsilon\n");
}
//使用 fabs 函数来比较两个浮点数 f 和 123.456f 是否在给定的容差(epsilon,EPS)范围内相等。
同时还可以使用
FLT_MIN
是 C/C++ 标准库中定义的一个宏,它代表了 float
类型能够表示的最小正非零值。这个值是由 <float.h>
(C 语言)或 <cfloat>
(C++ 语言)头文件提供的。需要注意的是,FLT_MIN
并不是 float
类型能表示的最小值(因为 float
类型还可以表示更小的负数以及零),而是它所能表示的最小正数。
FLT_MIN
的具体值取决于系统的浮点数表示方式和实现,但它通常是一个很小的正数,比如 1.175494351e-38F
(这是 IEEE 754 标准中 float
类型的一个常见最小值,但并非所有系统都遵循这个标准)。
如果你需要处理 float
类型的最小值(包括零和负数),你可能需要考虑使用 -FLT_MAX
(float
类型的最大负值)或者通过其他方式来判断一个 float
变量是否足够小以至于可以视为零。然而,直接比较浮点数和 FLT_MIN
(或任何其他特定的非零值)来检测“足够小”可能不是最佳选择,因为浮点数的表示和精度可能会引入误差。
另外,对于需要更高精度或更大范围的数值计算,你可能会考虑使用 double
或 long double
类型,它们分别有 DBL_MIN
和 LDBL_MIN
宏来表示它们的最小正非零值。
时间复杂度为O(1)的数据结构有
时间复杂度为O(1)的数据结构,主要是指在执行基本操作(如查找、插入、删除等)时,所需的时间不随数据量n的增加而增加,即保持常数时间。这样的数据结构在处理大量数据时非常高效。以下是一些常见的时间复杂度为O(1)的数据结构或操作场景:
1. 数组(Array)
- 特性:数组在内存中是连续存储的,通过索引可以直接访问数组中的元素,无需遍历,因此随机访问的时间复杂度为O(1)。
- 注意:虽然随机访问数组的时间复杂度是O(1),但如果需要根据值查找数组中的元素,则可能需要遍历整个数组,时间复杂度会变为O(n)。
2. 哈希表(Hash Table)
- 理想情况:在理想情况下,哈希表通过哈希函数将键(Key)映射到表中的一个位置(即数组的索引),然后直接访问该位置来存储或检索对应的值(Value)。因此,哈希表的插入、删除和查找操作的平均时间复杂度是O(1)。
- 实际情况:然而,由于哈希冲突(即不同的键可能映射到相同的位置)的存在,哈希表的性能可能会受到影响。为了解决冲突,常见的方法包括链地址法(Chaining)和开放地址法(Open Addressing)。在最坏情况下,如果哈希函数设计不当或冲突处理策略不佳,哈希表的时间复杂度可能会退化到O(n)。但在大多数情况下,设计良好的哈希表能够保持接近O(1)的操作效率。
3. 栈(Stack)
- 特定操作:栈是一种后进先出(LIFO)的数据结构,对于栈顶元素的插入(push)和删除(pop)操作,其时间复杂度是O(1)。这是因为这些操作只涉及栈顶元素的变动,而不需要遍历整个栈。
4. 队列(Queue)
- 特定操作:对于某些类型的队列(如双端队列deque),在特定操作(如两端插入和删除)上的时间复杂度可以接近O(1)。但是,对于普通的队列(如FIFO队列),在队尾插入元素和在队首删除元素的时间复杂度通常是O(1),但如果需要在队列中间进行插入或删除操作,则时间复杂度可能会增加。
5. 缓存(Cache)
- 特性:缓存通常使用哈希表或类似的数据结构来实现快速查找。当缓存命中时(即所需数据已在缓存中),访问时间可以视为O(1)。然而,当缓存未命中时,可能需要从较慢的存储介质(如硬盘)中加载数据,此时的时间复杂度将取决于数据加载的效率和存储介质的性能。
6. 静态数据结构
- 定义:静态数据结构是指一旦创建后就不再改变的数据结构。例如,如果有一个包含固定数量元素的数组,并且这些元素在程序的整个运行期间都不会改变,那么对这个数组的任何访问操作都可以认为是O(1)的,因为访问位置是固定的。
总结
时间复杂度为O(1)的数据结构通常具有高效的随机访问能力,但需要注意的是,这种高效性可能受到特定条件或操作场景的限制。在实际应用中,应根据具体需求选择合适的数据结构和操作方式。
Struct和Class的关系
区别
- 类型与存储
- 结构体是值类型(在C++中,尽管在C语言中它仅是数据类型的集合),其变量通常分配在栈上(尽管在C++中,如果结构体包含动态分配的成员,它也可能间接使用堆)。栈空间相对较小但访问速度快。
- 类是引用类型,其实例(对象)通常分配在堆上,而堆上存储实际数据,栈上存储引用(即对象的地址)。堆空间相对较大但访问速度较慢。
- 成员与方法
- 结构体主要用于封装数据,虽然C++中的结构体可以包含成员函数,但其成员(包括变量和方法)默认是公开的(public)。
- 类不仅可以封装数据,还可以封装方法(函数),并且其成员默认是私有的(private),需要显式声明为公开(public)或受保护(protected)才能被外部访问。
- 继承与多态
- 结构体通常不支持继承(在C语言中完全不支持,C++中虽然可以继承但默认为public继承,且由于其值类型的性质,继承的使用受到一定限制)。
- 类支持继承和多态,允许子类继承父类的属性和方法,并可以覆盖(重写)父类的方法以实现多态。
- 初始化与构造函数
- 结构体的初始化相对简单,C++中虽然可以添加构造函数,但始终存在一个默认的无参构造函数(不可重写或覆盖)。
- 类的初始化可以通过构造函数完成,一旦定义了带参数的构造函数,默认的无参构造函数就不存在了,需要显式定义。
- 关键字与特性
- 在C++中,某些关键字(如abstract、sealed、protected)在类中可以使用,但在结构体中有限制或不可使用。Static关键字可用于声明静态类,但不可用于声明静态结构体。
联系
- 定义与使用:结构体和类的定义方式非常相似,都使用关键字(struct/class)后跟花括号内的成员声明。它们的使用方式也类似,都是创建自定义数据类型的实例。
- 容器类型:两者都是容器类型,可以包含其他数据类型作为成员,包括基本数据类型、结构体、类(在类中)等。
- 成员访问:结构体和类的成员都可以有各自的存取范围(public、private、protected),从而控制成员的访问权限。
- 接口实现:结构体和类都可以实现接口,从而提供一组特定的方法供外部调用。
- 事件与委托:在支持这些特性的编程环境中,结构体和类都可以声明和触发事件,并且可以声明委托(Delegate)来定义事件处理函数的签名。
C语言总结
头文件引入时的细节
在C语言中,双引号(“”)和尖括号(<>)在包含(include)头文件时扮演了不同的角色,它们之间的主要区别在于编译器如何查找和处理这些头文件。
-
双引号(“”):
当使用双引号来包含头文件时,如#include "filename.h"
,编译器首先会在当前文件所在的目录下查找名为filename.h
的文件。如果在当前目录下找不到该文件,编译器会继续在标准库路径(或指定的其他路径)中查找。这种方式常用于包含用户自定义的头文件或第三方库的头文件,因为这些文件可能不在标准库路径中。 -
尖括号(<>):
使用尖括号来包含头文件时,如#include <filename.h>
,编译器会直接在标准库路径(或指定的其他路径)中查找名为filename.h
的文件,而不会首先在当前目录下查找。这种方式主要用于包含标准库的头文件,因为这些文件通常都位于编译器的标准库路径中。
总结:
- 使用双引号时,编译器会先在当前目录下查找头文件,若未找到,则继续在标准库路径中查找。
- 使用尖括号时,编译器直接在标准库路径中查找头文件,不会在当前目录下查找。
选择使用哪种方式取决于你正在包含的头文件的性质:如果是标准库的头文件,推荐使用尖括号;如果是用户自定义的头文件或第三方库的头文件,推荐使用双引号。这样做可以提高代码的可移植性和编译器的查找效率。
1.常用的c变量的定义方式
-
一个整型数(An integer)
int a; // An integer
-
一个指向整型数的指针(A pointer to an integer)
int *a; // A pointer to an integer
-
一个指向指针的的指针,它指向的指针是指向一个整型数(A pointer to a pointer to an integer)
int **a; // A pointer to a pointer to an integer
-
一个有10个整型数的数组(An array of 10 integers)
int a[10]; // An array of 10 integers
-
一个有10个指针的数组,该指针是指向一个整型数的(An array of 10 pointers to integers)
int *a[10]; // An array of 10 pointers to integers
-
一个指向有10个整型数数组的指针(A pointer to an array of 10 integers
int (*a)[10]; // A pointer to an array of 10 integers
-
一个指向函数的指针,该函数有一个整型参数并返回一个整型数(A pointer to a function that takes an integer as an argument and returns an integer)
int (*a)(int); // A pointer to a function a that takes an integer argument and returns an integer
-
一个有10个指针的数组,该指针指向一个函数,该函数有一个整型参数并返回一个整型数( An array of ten pointers to functions that take an integer argument and return an integer )
int (*a[10])(int); // An array of 10 pointers to functions that take an integer argument and return an integer
2.sizeof运算符和strlen函数的区别
-
strlen是一个函数,求字符长度,不看容器多大,明确里面的元素,只看内容字符长度
-
sizeof是一个内置运算符,,看容器大小
int main()
{
char *src = "helloworld";
char dest[20];
char nsrc[20] = "helloworld";
sizeof(src);//8个字节,64位操作系统
strlen(src);//10
sizeof(nsrc);//20
strlen(nsrc);//10
sizeof(dest);//20
strlen(dest);//不确定,数组没有初始化
return 0;
}
3.数组
1.普通数组
- 数组大小必须是常量表达式(常量或符号常量),其值必须为正,不能为变量。最好用宏或枚举来定义来定义,以适应未来可能的变化
- 数组名不能当左值,数组类型做右值使用时,自动转换成指向数组首元素的指针
- 数组名当函数参数传递,会被退化为指针,就是将数组的首地址传递给函数,如果要求不退化指针?结构体封装(因为结构体按值传递)或者STL模板的Vector容器(仅限于C++)
- 任何一个数组的名字是一个常量指针,其值是该数组的首元素的地址值
- 下标法:如a[j]或p[j]形式,比较直观
- 指针法:如 *(a+j) 或 *(p+j) 形式,比较高效
2.指针数组
-
指针数组是一种特殊的数组,其元素是指向某种数据类型的指针。指针数组的每个元素都可以存储一个地址,这个地址指向内存中的另一个位置,那个位置存储了特定类型的数据。
-
定义形式:类型关键字 *数组名[数组长度];
-
char *pStr[5];
指针数组排序:只交换地址,不交换具体内容
char *ptr[N] = {"Pascal","Basic","Fortran",
"Java","Visual C"};
char *tmp;
for (i = 0; i < N - 1; i++)
{
for (j = i + 1; j < N; j++)
{
if (strcmp(ptr[j], ptr[i]) < 0)
{
temp = ptr[i];
ptr[i] = ptr[j];
ptr[j] = temp;
}
}
}
3.二维数组
1.代码示例
#include <stdio.h>
int main()
{
char a[10] = "Sunday";
char *p ;
char weekDay[7][10] =
{"Sunday", "Monday", "Tuesday","Wednesday",
"Thursday", "Friday", "Saturday"};
for (int i = 0; i < 7; i++)
{
p = weekDay[i];
printf("%s\n", p);
}
for (int i = 0; i < 7; i++)
{
printf("%s\n", weekDay[i]);
}
return 0;
}
2.注意事项
- 内存连续性:二维数组在内存中是连续存储的。这意味着,如果你有一个
int arr[3][4];
,那么整个数组将占用一个连续的3 * 4 * sizeof(int)
字节的内存块。 - 行优先顺序:在C语言中,二维数组是以行优先的顺序存储的。即,数组的第一行完全存储在内存中,紧接着是第二行,依此类推。
- 固定大小:二维数组的大小在编译时就必须确定,并且之后不能改变。如果你需要一个可以在运行时改变大小的二维数据结构,你可能需要使用指针数组(二维指针)和动态内存分配。
- 初始化:如果二维数组在全局或静态存储区中定义,并且没有被显式初始化,那么它的所有元素都将被自动初始化为0(对于数值类型)。但是,如果你在函数内部定义了局部二维数组而没有初始化,那么它的初始值是未定义的。
4.指针
1.指针定义
- 指针也是一种数据类型
- 指针变量:具有指针类型的变量,专门存放地址数据的变量
- 变量的指针:变量的地址
- 不能返回局部变量的指针
- 指针指向非其定义时声明的数据类型,将引起warning
2.注意野指针
-
“野指针”不是NULL指针,是指向“垃圾”内存的指针,“野指针”的危险之处在于if语句对它不起作用。
-
任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是不确定的。所以,指针变量在创建的同时应当被初始化,要不将指针设置为NULL,要么让它指向合法的内存。
-
指针p被free或者delete之后,应设置为NULL。
-
指针操作超越了变量的作用范围
-
程序不能返回指向栈内存的指针或引用
-
可以通过指针访问数组中的每个元素;
3.通用类型指针Void
-
void*类型的指针可以指向任意类型的变量,通用类型指针,不能用来取值
-
编程时经常需要一种通用指针,可以转换为任意其它类型的指针,任意其它类型的指针也可以转换为通用指针,最初C语言没有void *类型,就把char *当通用指针,需要转换时就用类型转换运算符(),ANSI在将C语言标准化时引入了void *类型,void *指针与其它类型的指针之间可以隐式转换,而不必用类型转换运算符。
4.指针与const限定符
const
也可以用来声明指针所指向的数据是常量,或者指针本身是常量。
1.示例
是一个指向const int型的指针,a所指向的内存单元不可改写,所以(*a)++是不允许的,但a可以改写,所以a++是允许的。
int * const a;
a是一个指向int型的const指针,*a是可以改写的,但a不允许改写。
int const * const a;
a是一个指向const int型的const指针,因此*a和a都不允许改写
char c = 'a';//可读可写
const char *pc = &c;//只读不碍事
但是:不可行
const char c = 'a';//只读
char *pc = &c;//操作受限制
2.注意事项
- 初始化:使用
const
声明的变量必须在声明时初始化,因为之后无法修改其值。 - 存储类别:
const
声明的变量默认具有外部链接(除非在块内部声明),如果想要内部链接,可以使用static const
。 - const 与 volatile:
const
表明值不会改变,但与volatile
结合使用时,表示虽然值不会改变,但可能会被外部因素(如硬件)改变。
5.二维指针
1.定义
二维指针(也称为指向指针的指针),这意味着,二维指针可以用来存储一个指针的地址,而这个指针本身又指向另一个变量
2.代码示例
#include <stdio.h>
int main()
{
int i;
char *ptr[] = {"Pascal", "Basic", "Fortran", "Java", "Visual C"};
char **p;指向指针类型的指针叫二维指针
p = ptr;//ptr是指针类型
for (i=0; i<5; i++)
{
printf("%s\n",
*p);
p++;
}
return 0;
}
3.二维数组和二维指针
在C/C++中,二维数组在内存中是连续存储的,但是通过二维数组名访问时,编译器会将其视为一个指向数组首元素的指针,该首元素本身也是一个指向数组(即第二维)的指针。但是,直接使用二维数组名作为指针时,其类型并不是指针的指针(**
),而是指向包含n个元素的数组的指针(例如,int (*)[n]
)。然而,我们可以通过指针的指针来模拟或操作二维数组。
#include <stdio.h>
int main() {
int arr[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
// 指向arr的指针,其类型为指向包含4个整数的数组的指针
int (*ptr)[4] = arr;
// 转换为二维指针来访问
int** pp = (int**)&arr[0][0]; // 注意:这种转换虽然可以工作,但不是标准或推荐的做法
// 使用ptr访问
printf("%d\n", (*ptr)[1]); // 输出5
// 假设我们知道pp如何正确工作(注意:下面的访问方式基于上述非常规转换)
printf("%d\n", pp[1][1]); // 尝试输出7,但注意这种方法依赖于内存布局和编译器行为
// 更安全的二维指针使用方式(动态分配)
int** dynamicArray = (int**)malloc(3 * sizeof(int*));
for (int i = 0; i < 3; i++) {
dynamicArray[i] = (int*)malloc(4 * sizeof(int));
for (int j = 0; j < 4; j++) {
dynamicArray[i][j] = i * 4 + j + 1;
}
}
// 访问动态分配的二维数组
printf("%d\n", dynamicArray[1][2]); // 输出9
// 释放内存
for (int i = 0; i < 3; i++) {
free(dynamicArray[i]);
}
free(dynamicArray);
return 0;
}
6.指针运算
指针运算不能乱算
-
指针的加减运算是以其指向的类型的字长为单位的
-
一般只进行指针和整数的加减运算,同类型指针之间的减法运算
-
其它运算,比如乘法、除法、浮点运算、指针之间的加法等,并无意义,所以也不支持
int类型
#include <stdio.h>
int main()
{
int a[10]={1,2,3,4,5,6,7,8,9,10};
int i;
int *p=a;
p+1//int类型 步长为4中间间隔4个字节;
printf("%ld\n",(int *)0x40 - (int*)0x20);
//俩个指针相减,之后要除步长。
//俩个指针可以减,不能加
//不同类型指针不能作减法运算
return 0;
}
2.关系运算
只有指向同一种数据类型的两个指针才能进行关系运算(减法yu)。
-
值为1或0
-
p > q p < q p == q
-
指针不与非指针量进行比较,但可与NULL(即0值)进行等或不等的关系运算判断p是否为空指针
-
p == NULL
-
p != NULL
3.赋值运算
-
指针在使用前一定要赋值
-
为指针变量赋的值必须是一个地址
#include<stdio.h>
int main()
{
int a = 3;
int b = 5;
int *p = &a;
int *pa;
p = &b;
int **pp;
pp = &p;//指向p的二维指针,p的地址赋给pp
pa = p;//都指向b,p的值赋给pa
return 0;
}
7.指向函数的指针
int (*pfunc)(int, int)//定义了一个函数指针pfunc,它指向的函数必须是:返回值为int类型,有两个int类型的参数
代码示例
#include <stdio.h>
#include <stdlib.h>
int add(int a, int b)
{
int total = a + b;
return total;
}
int main(int argc, char *argv[])
{
int (*p)(int, int) = add;
int total = p(3, 5);
printf("%d\n", total);
total = (*p)(3, 5);
printf("%d\n", total);
return 0;
}
8.指针和数组的注意事项
- 指针变量是变量
-
特别1:可以对此变量进行*操作
-
特别2:此变量的加减法有些特殊
-
数组成员是变量
-
指针是一种数据类型
-
数组名可看作常量指针
-
指针也可看作数组名
6.对它们,只要按照变量和类型的一般原则使用就可以,没 有多少特殊化的地方
5.结构体
1.定义
用户自定义的数据类型,把关系紧密且逻辑相关的多种不同类型的变量组织到统一的名字之下,也称复合数据类型,这种类型的变量占用相邻的一段内存单元,按值传递,结构体是变量,里面存放地址,用箭头改成按地址传递。
2.定义方式
- 先定义结构体类型再定义变量名
- 在定义类型的同时定义变量名
- 直接定义结构体变量(不出现结构体名)
3.结构体嵌套
在C语言中,结构体不仅可以包含基本数据类型作为成员,还可以包含其他结构体作为成员,这被称为结构体的嵌套(Nested Structures)。结构体嵌套允许你构建更复杂的数据结构,这些结构可以模拟现实世界中的复杂关系或对象。
定义嵌套结构体
当你定义一个结构体,并希望它的一个或多个成员是另一种结构体类型时,你就创建了嵌套结构体。
#include <stdio.h>
// 定义Address结构体
struct Address {
char street[100];
char city[50];
char state[2];
int zip;
};
// 定义Person结构体,它包含一个Address类型的成员
struct Person {
char name[50];
int age;
struct Address addr; // 嵌套结构体
};
int main() {
struct Person person;
// 初始化Person结构体
strcpy(person.name, "John Doe");
person.age = 30;
// 初始化嵌套的Address结构体
strcpy(person.addr.street, "123 Elm Street");
strcpy(person.addr.city, "Somewhere");
strcpy(person.addr.state, "CA");
person.addr.zip = 12345;
// 打印Person信息
printf("Name: %s\n", person.name);
printf("Age: %d\n", person.age);
printf("Address: %s, %s %s %d\n", person.addr.street, person.addr.city, person.addr.state, person.addr.zip);
return 0;
}
在这个例子中,Person
结构体有一个name
(字符数组)、一个age
(整型)和一个addr
(Address
类型)作为成员。Address
结构体则包含了街道、城市、州和邮编等信息。通过将Address
结构体作为Person
结构体的一个成员,我们能够在Person
结构体中嵌入完整的地址信息。
访问嵌套结构体的成员
要访问嵌套结构体的成员,你需要使用点操作符(.
)来连续访问每个结构体的成员。在上面的例子中,我们通过person.addr.street
来访问person
的addr
成员中的street
成员。
结构体指针与嵌套结构体
当使用结构体指针时,访问嵌套结构体的成员的方式会稍有不同。你需要先通过指针访问外层结构体,然后再通过外层结构体的成员(即另一个结构体)访问其内部成员,但此时应使用箭头操作符(->
)来访问。
struct Person *p = &person;
// 使用结构体指针访问嵌套结构体成员
printf("Name: %s\n", p->name);
printf("Address: %s, %s %s %d\n", p->addr.street, p->addr.city, p->addr.state, p->addr.zip);
在这个例子中,我们首先定义了一个指向Person
结构体的指针p
,并将其初始化为person
的地址。然后,我们使用p->
语法来访问p
指向的Person
结构体及其嵌套的Address
结构体中的成员。
4.结构体内存空洞
1.结构体内存分配
- 对齐规则:结构体的每个成员变量在内存中的起始位置必须是其自身大小的整数倍。这样做是为了提高内存访问的效率,避免因为跨界访问而导致性能下降。
- 结构体大小:结构体的大小取决于结构体中最大成员变量的大小,同时还要考虑对齐规则。
- 填充字节:为了满足对齐规则,编译器可能会在结构体的成员变量之间插入一些填充字节,使得每个成员变量的起始地址都符合对齐要求。
结构体的大小可能会受到编译器、编译选项和平台的影响,因此最好使用 sizeof
运算符来确定结构体的大小。
2.代码示例
#include<stdio.h>
typedef struct student
{
int a;
char c;
short b;
}student;
int main()
{
printf("%lu\n", sizeof(student));
return 0;
}
6.动态内存分配malloc和free
malloc
和 free
是 C 语言标准库中的函数,用于在堆(heap)上动态地分配和释放内存。这些函数定义在 <stdlib.h>
头文件中。
malloc
函数用于在堆上分配指定大小的内存块,并返回指向该内存块的指针。如果内存分配成功,malloc
返回一个非空指针;如果分配失败(例如,由于内存不足),则返回 NULL
。
函数的原型如下:
void *malloc(size_t size);
其中,size
参数表示要分配的字节数。由于 malloc
返回一个 void
指针,因此通常需要将其转换为适当的类型指针来存储返回的内存地址。
例如:
int *ptr = (int *)malloc(sizeof(int) * 10);
if (ptr == NULL) {
// 内存分配失败,处理错误
} else {
// 使用 ptr 指向的内存块
}
free
函数则用于释放之前通过 malloc
(或其他相关函数如 calloc
或 realloc
)分配的内存块。调用 free
后,指针本身并不会被自动设置为 NULL
,因此通常建议显式地将指针设置为 NULL
,以避免悬挂指针(dangling pointer)的问题。
函数的原型如下:
void free(void *ptr);
其中,ptr
是指向要释放的内存块的指针。传递 NULL
给 free
是安全的,并且不会有任何效果。
例如:
// 假设 ptr 是一个之前通过 malloc 分配的内存块的指针
free(ptr);
ptr = NULL; // 避免悬挂指针
正确使用 malloc
和 free
是编写健壮的 C 语言程序的关键部分,因为它们涉及到内存管理,而内存管理是编程中一个常见且容易出错的领域。不正确的内存管理可能导致内存泄漏、程序崩溃或其他未定义行为。
内存泄漏
1.内存动态分配后,当它不再被使用时未被释放。(狭义)
2.滥用静态变量数据内存会导致内存大量积累,导致内存泄漏。
3.内存碎片(频繁申请和释放),
7.链表基本操作
1.头插法建立链表
不带头节点
void insert_node(node *p)//头插法建立链表
{
p->next=head;
head=p;
}
带头结点
void insert_node(node *p)
{
p->next = head->next;
head->next = p;
}
2.依次遍历打印链表
不带头结点
void traverse()//挨个打印出链表的元素
{
node *p = head;
while (p!=NULL)
{
printf("%d",p->data);
p=p->next;
}
printf("\n");
}
带头结点
void traverse()
{
node *p = head->next;//head节点之后的才是链表元素
while (p != NULL)
{
printf("%d ", p->data);
p = p->next;
}
printf("\n");
}//依次打印
3.查找链表对应的值
不带头结点
node * search(int target)
{
node *p = head;
while (p!=NULL)
{
if (p->data == target)
{
return p;
}
p=p->next;
}
return NULL;//到最后没找到返回空
}
带头结点
node *search(int target)//和遍历很类似
{
node *p = head->next;
while (p != NULL)
{
if (p->data == target)
{
return p;
}
p = p->next;
}
return NULL;
}
4.删除对应链表元素
删除链表要找前驱节点pre
不带头结点
void rm_node(node *p)
{
//分为俩个情况讨论,删除的节点为头节点或者其他节点
if (p == head)
{
head=head->next;//无论是不是头节点都这么用
p->next=NULL;
return;
}
node *pre=head;//删除要找到前驱节点,pre等于head,等于头节点
//如果pre->next==null;pre本就是前驱节点,pre->next为空代表要删除的节点不存在了 ,链表要结束了
while (pre->next !=NULL)
{
if (pre->next==p)
{
pre->next=p->next;
break;
}
pre=pre->next;
}
}
带头结点
void rm_node(node *p)
{
node *pre = head;
while (pre->next != NULL)
{
if (pre->next == p)
{
pre->next = p->next;
p->next = NULL;
break;
}
pre = pre->next;
}
}
5.销毁链表
不带头节点
void destory()
{
node *p;
while (head!=NULL)
{
p=head;
head=head->next;
free_node(p);
}
}
//带头节点和不带头节点
void destroy()
{
node *p;
while (head->next != NULL)
{
p = head->next;
printf("%d ", p->data);
head->next = p->next;
p->next = NULL;
free_node(p);
}
printf("----\n");
}
带头结点
void destroy()//与删除类似
{
node *p;
while (head->next != NULL)
{
p = head->next;
head->next = p->next;
p->next = NULL;
free_node(p);
}
printf("----\n");
}
6.插入排序
不带头节点
void insert_node_l2b(node *p)
{
node *pre = head;
if (p->data<pre->data)
{
p->next=head;
head=p;
}
else{
while (pre->next != NULL)
{
if (p->data < pre->next->data)
{
break;
}
pre = pre->next;
}
p->next = pre->next;
pre->next = p;
}
}
带头结点
void insert_node_l2b(node *p)
{
node *pre = head;
while (pre->next != NULL)
{
if (p->data < pre->next->data)
{
break;
}
pre = pre->next;
}
p->next = pre->next;
pre->next = p;
}
7.链表倒置
不带头节点
void reverse()
{
node *p;
node *pre = head->next;
head->next = NULL;//将head结点分离出来
while (head != NULL)
{
p = pre;
pre =pre->next;
p->next =NULL;
//从头到尾依次断开之后用头插法倒置
insert_node(p);
}
}
带头结点
void reverse()
{
node *p;
node *nhead = head->next;
head->next = NULL;//将head结点分离出来
while (nhead != NULL)
{
p = nhead;
nhead = nhead->next;
p->next =NULL;
//从头到尾依次断开之后用头插法倒置
insert_node(p);
}
}
8.循环链表
经典例子约瑟夫环
数组实现
#include <stdio.h>
#include <stdlib.h>
int josph(int n)
{
int i;
int total = 0;
int left = n;
int last = 0;
int *people = (int *)malloc(n*sizeof(int));
if (people == NULL)
{
printf("malloc failed\n");
exit(1);
}
for (i = 0; i < n; i++)
{
people[i] = i + 1;
}
i = 0;
while (left > 1)
{
if (people[i] != 0)
{
total++;
if (total == 3)
{
printf("%d ", people[i]);
people[i] = 0;
left--;
total = 0;
}
}
i++;
i = i % n;
}
printf("\n");
for (i = 0; i < n; i++)
{
if (people[i] != 0)
{
last = people[i];
break;
}
}
free(people);
return last;
}
int main()
{
int n;
int last;
scanf("%d", &n);
last = josph(n);
printf("%d\n",last);
return 0;
}
头指针约瑟夫环
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
typedef struct node
{
int data;
struct node *next;
}node;
node *head = NULL;
node *mk_node(int data)
{
node *p = (node *)malloc(sizeof(node));
if (p == NULL)
{
printf("malloc failed\n");
exit(1);
}
p->data = data;
p->next = NULL;
return p;
}
void free_node(node *p)
{
free(p);
}
int josph(int n)
{
int i;
node *p;
node *tail;
node *pre;
int last ;
p = mk_node(1);
head = p;
tail = p;
for (i = 1; i < n; i++)//尾插插入
{
p = mk_node(i+1);
tail->next = p;
tail = p;
}
//traverse();
tail->next = head;
int total = 1;
pre = head;
while (pre->next != pre)
{
total++;
if (total == 3)
{
p = pre->next;
pre->next = p->next;
printf("%d ", p->data);
total = 1;
free_node(p);
}
pre = pre->next;
}
printf("\n");
last = pre->data;
free_node(pre);
return last;
}
int main()
{
int n;
int last;
scanf("%d", &n);
last = josph(n);
printf("%d\n",last);
return 0;
}
头节点循环链表
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
typedef struct node
{
int data;
struct node *next;
}node;
node sentry = {0, NULL};
node *head = NULL;
void init()
{
head = &sentry;
head->next = head;
}
node *mk_node(int data)
{
node *p = (node *)malloc(sizeof(node));
if (p == NULL)
{
printf("malloc failed\n");
exit(1);
}
p->data = data;
p->next = NULL;
return p;
}
void free_node(node *p)
{
free(p);
}
void insert_node(node *p)
{
p->next = head->next;
head->next = p;
}
void insert_node_l2b(node *p)
{
node *pre = head;
while (pre->next != head)
{
if (p->data < pre->next->data)
{
break;
}
pre = pre->next;
}
p->next = pre->next;
pre->next = p;
}
void traverse()
{
node *p = head->next;
while (p != head)
{
printf("%d ", p->data);
p = p->next;
}
printf("\n");
}
node *search(int target)
{
node *p = head->next;
while (p != head)
{
if (p->data == target)
{
return p;
}
p = p->next;
}
return NULL;
}
void rm_node(node *p)
{
node *pre = head;
while (pre->next != head)
{
if (pre->next == p)
{
pre->next = p->next;
p->next = NULL;
break;
}
pre = pre->next;
}
}
void destroy()
{
node *p;
while (head->next != head)
{
p = head->next;
printf("%d ", p->data);
head->next = p->next;
p->next = NULL;
free_node(p);
}
printf("----\n");
}
void reverse()
{
node *p;
node *nhead = head->next;
head->next=head;
while (nhead != head)
{
p = nhead;
nhead = nhead->next;
p->next = NULL;
insert_node(p);
}
}
void insert_sort()
{
node *p;
node *nhead = head->next;
head->next = NULL;
while (nhead != NULL)
{
p = nhead;
nhead = nhead->next;
p->next = NULL;
insert_node_l2b(p);
}
}
int main()
{
node *p;
int target;
int i;
srand(time(NULL));
init();
for (i = 0; i < 10; i++)
{
p = mk_node(i + 1);
insert_node(p);
}
traverse();
scanf("%d", &target);
p = search(target);
if (p == NULL)
{
printf("Can't find %d\n", target);
}else
{
printf("%p %d %d\n", p, p->data, target);
rm_node(p);
free_node(p);
}
traverse();
destroy();
traverse();
return 0;
}
9.Typedef和宏定义
#define STRING1 char *
typedef char * STRING2;
int main()
{
STRING1 s1,s2;//char * s1,char s2;
STRING2 s1,s2;//char * s1,char * s2;
}
typedef
和宏定义是 C/C++ 中用于创建别名和宏替换的两种不同机制。它们在某些方面有相似之处,但在功能和使用上有一些重要区别。
typedef
typedef
是 C/C++ 中的关键字,用于为现有数据类型创建新的名称(别名)。- 通过
typedef
可以为数组、结构体、枚举、指针等数据类型创建别名,提高代码的可读性和可维护性。 typedef
创建的别名是新的类型名称,可以在程序中像使用原始类型一样使用。typedef
通常用于定义复杂数据类型,使代码更易于理解和维护。
示例:
typedef int Integer; // 创建 int 的别名 Integer
Integer num = 10; // 使用 Integer 作为 int 的别名
宏定义
- 宏定义是 C/C++ 中的预处理指令,用于在编译前进行文本替换。
- 通过宏定义可以创建宏,将某个标识符替换为指定的文本,可以是常量、表达式、函数等。
- 宏定义不会创建新的数据类型,而是在编译前将标识符替换为指定的文本。
- 宏定义可以接受参数,实现类似函数的功能,但它是在预处理阶段进行简单的文本替换。
示例:
#define PI 3.14159 // 定义 PI 为 3.14159
float radius = 5.0;
float area = PI * radius * radius; // 在编译前将 PI 替换为 3.14159
区别
typedef
创建类型别名,宏定义进行文本替换。typedef
创建的别名是新的数据类型,而宏定义只是简单的文本替换。typedef
在编译后会进行类型检查,而宏定义只是简单的文本替换,不进行类型检查。typedef
可以提高代码的可读性和可维护性,而宏定义可能会导致代码可读性下降。
10.位字段
位字段是C语言中一种存储结构,不同于一般结构体的是它在定义成员的时候需要指定成员所占的位数。
◼ 它主要用于一些使用空间很宝贵的程序设计中,如嵌入式程序设计。
struct bit_field
{
unsigned int a:5;
unsigned int b:3;
unsigned int c:20;
unsigned int d:4;
};
int main()
{
struct bit_field x;
x.a = 12;
x.b = 7;
x.c = 1024;
x.d = 13;
printf("%d\n",x.a);
printf("%d\n",sizeof (struct bit_field));
return 0;
}
/*在如上定义中,bit_field结构体只占用一个word的空间,即4个字节。其中成员a占用5位,成员b占用3位,成员c占用20位,成员d占用4位。最好使用unsigned 类型*/
-
对于位字段中的成员不能用位操作符进行运算,因为它们没有地址。考虑到字节存放的大端小端的问题,使用位字段定义的数据不能在不同字节顺序的机器之间移动。因此,从理论上来说,使用位字段的程序是不可移植的。
-
其实,我们完全可以用位操作来实现位字段的功能,它们在编译后的代码与效率上是一样的。
服务器一般用大端,intel小端
//如何判断是大小端,会报错预警,不碍事
int main()
{
int x=0x12345678;
char *p=&x;
//定义了一个字符指针 p,并将其指向 x 的地址。由于 char 类型通常占用 1 个字节,p 实际上指向了 x 的第一个字节(在内存中的最低地址)。
if(*p ==0x78)
{
printf("little\n");
}
else if(*p ==0x12){
printf("big\n");
}
else
{
printf("unknow\n");
}
return 0;
//经典考题
}
位字段可以进行位操作
例如:设置bits的5-9位为value
//位操作
unsigned int bits;
bits &= ~(0x1f << 5) /* 将5-9位设置为0 */
bits |= value << 5 /* 设置5-9位的值 */
//位字段
struct
{
unsigned int a : 5;
unsigned int b : 5;
unsigned int c : 22;
} bits;
bits.b = value;
11.共用体,或称为联合(Union)
union number
{
short x;
char ch;
float y;
};
-
基本上和struct一样
-
x、ch和y处于同样的地址
-
sizeof(union xxx)取决于占空间最多的那个成员变量
特点
-
同一内存单元在每一瞬时只能存放其中一种类型的成员;并非同时都起作用
-
起作用的成员是最后一次存放的成员
-
不能作为函数参数
-
共用体判断大小端示例
union ss
{
int a;
char b[4];
};
int main()
{
union ss s1;
s1.a=0x12345678;
//为什么用b数组来判断是否为大小端,因为ab在一块内存存放
if(s1.b[0] ==0x78)
{
printf("little\n");
}
else if(s1.b[0]==0x12){
printf("big\n");
}
else
{
printf("unknow\n");
}
return 0;
}
12.文件操作
在C语言中,文件操作是通过一组特定的函数来完成的,这些函数允许程序创建、打开、读取、写入、关闭文件。下面是一些常用的C语言文件操作函数以及它们的基本用法:
1.打开文件
fopen
函数用于打开一个文件。它的原型是:
c复制代码
FILE *fopen(const char *filename, const char *mode);
filename
是要打开文件的名称(包括路径)。mode
是打开文件的模式,例如"r"
表示只读,"w"
表示写入(覆盖已有文件),"a"
表示追加,"r+"
表示读写等。
2.读取文件
读取文件时,可以使用多种函数,具体取决于你需要读取的数据类型:
fgetc
:从文件读取一个字符。fgets
:从文件读取一行。fscanf
:格式化地从文件读取数据。fread
:从文件读取指定数量的数据项。
3.写入文件
写入文件时,同样有多种函数可供选择:
fputc
:写入一个字符到文件。fputs
:写入一行文本到文件。fprintf
:格式化地写入数据到文件。fwrite
:写入指定数量的数据项到文件。
4.关闭文件
使用完文件后,应使用 fclose
函数关闭文件:
c复制代码
int fclose(FILE *stream);
stream
是由 fopen
返回的 FILE
指针。
#include <stdio.h>
#include <stdlib.h>
int main()
{
int ch;
FILE *fp = fopen("1.c", "r");
if (fp == NULL)
{
perror("open");
exit(1);
}
while ((ch = fgetc(fp)) != EOF)//依次读取直到结束
{
putchar(ch);
}
fclose(fp);
return 0;
}
读取时,没有文件时的报错和文件权限不够的报错,如果文件本身不允许读取,那么文件操作就无法进行,如果文件本身允许读写,那么文件操作才可以继续下去,切记不可放大操作。指针在内存操作,文件操作在磁盘操作
代码拷贝
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
if (argc < 3)
{
printf("usage:./a.out srcfile destfile\n");
exit(1);
}
int ch;
FILE *fp_src = fopen(argv[1], "r");
if (fp_src == NULL)
{
perror("open");
exit(1);
}
FILE *fp_dest = fopen(argv[2], "w");
if (fp_dest == NULL)
{
perror("open");
exit(1);
}
while ((ch = fgetc(fp_src)) != EOF)
{
fputc(ch, fp_dest);
}
fclose(fp_src);
fclose(fp_dest);
return 0;
}
如果将小空间代码拷贝到已经有内容的大空间代码中,会截断,原来大文件剩余空间都会删除
5.EOF
EOF(End of File)是一个在C语言中定义的宏,用于表示文件结束的标志。在标准I/O库中,EOF通常被定义为一个负整数,通常是-1。当使用标准输入函数(如getchar()
、fgetc()
等)读取文件时,如果到达文件末尾,这些函数会返回EOF以表示文件结束。
在C语言中,EOF通常用于以下情况:
- 文件结束标志:当读取文件时,EOF用于表示已经到达文件的末尾,没有更多的数据可供读取。
- 错误指示:有时候EOF也可以表示发生了错误,导致无法继续读取数据。
当使用getchar()
、fgetc()
等函数读取文件时,应该始终检查返回值是否等于EOF,以确保正确处理文件结束的情况。
EOF返回值,为什么定义为int?
在C语言中,字符实际上是以整数形式存储的,具体来说是存储为它们对应的ASCII码值。ASCII码值是一个非负整数,范围从0到127(对于基本的7位ASCII)。因此,使用char
类型来存储字符在大多数情况下是足够的。
然而,当涉及到输入/输出操作,特别是文件读取时,有一个特殊的情况需要考虑:文件结束符(EOF)。在C标准库中,EOF用于表示输入流的结束。EOF的值在C标准中定义为负数(-1),以确保它不会与任何有效的字符ASCII码值冲突。
现在,问题来了:char
类型在某些系统上可能是有符号的,而在其他系统上可能是无符号的。这意味着在一个有符号char
的系统上,-1可以安全地存储为char
,但在一个无符号char
的系统上,-1会被转换为一个大正数(通常是255,因为-1的二进制补码表示形式在转换为无符号时正好是255)。因此,如果使用char
来存储从getchar
或其他输入函数返回的值,并且这个值恰好是EOF,那么在无符号char
的系统上,你将无法正确检测到文件结束,因为EOF会被误解为一个有效的字符。
为了解决这个问题并确保跨平台的兼容性,C标准库的设计者选择让getchar
和其他输入函数返回int
类型,而不是char
。这样,即使EOF的值为-1,它也可以安全地存储在int
类型的变量中,而不会受到char
类型有符号或无符号属性的影响。因此,使用int
来存储从输入函数返回的值可以确保正确检测到文件结束,无论char
类型是有符号还是无符号。
简而言之,将输入函数的返回类型定义为int
是为了确保能够安全地存储和检测包括EOF在内的所有可能的返回值,无论底层系统的char
类型实现如何。这是一种为了跨平台兼容性和正确性而采取的预防措施。
6.标准I/O操作
-
文件操作中的"mode":
-
文件打开模式
:在C语言中,打开文件时需要指定打开文件的模式,常见的文件打开模式包括:
"r"
: 只读模式,打开一个文本文件,文件必须存在,文件指针位于文件开头。"w"
: 只写模式,若文件存在则清空文件内容,若文件不存在则创建新文件,文件指针位于文件开头。"a"
: 追加模式,若文件存在则文件指针位于文件末尾,若文件不存在则创建新文件。"r+"
: 读写模式,文件必须存在,文件指针位于文件开头。"w+"
: 读写模式,若文件存在则清空文件内容,若文件不存在则创建新文件,文件指针位于文件开头。"a+"
: 读写模式,若文件存在则文件指针位于文件末尾,若文件不存在则创建新文件。
2.getchar 和fget的区别
在C语言中,
getchar()
和fgetc()
函数都用于从输入流中读取字符,但它们之间有一些重要的区别,特别是在读取位置方面:getchar从终端中取出,fgetc在当前文件中取出**
getchar()
**:getchar()
函数是从标准输入流(stdin)中读取一个字符。- 每次调用
getchar()
函数,它会从标准输入流中读取下一个字符,并且会移动读取位置到下一个字符。 - 由于
getchar()
函数是针对标准输入流的,因此它通常用于从键盘输入数据。
**
fgetc()
**:fgetc()
函数是通用的,可以从任何指定的输入流中读取一个字符,比如文件流(FILE*)。- 调用
fgetc()
函数时,需要指定要读取的输入流作为参数,例如fgetc(file)
,其中file
是一个文件指针。 - 每次调用
fgetc()
函数,它会从指定的输入流中读取下一个字符,并且会移动读取位置到下一个字符。 - 因为
fgetc()
函数可以操作不同的输入流,所以它可以用于从文件或其他输入源中读取数据。
-
参数不同:
getchar
函数不需要参数,它直接从标准输入流(stdin)中读取一个字符。fgetc
函数需要一个参数,即指向已打开文件的指针,用于从指定文件流中读取一个字符。
-
返回值不同:
getchar
函数的返回类型是int
,它返回读取的字符的ASCII码值,如果到达文件结尾或出现错误,则返回EOF(-1)。fgetc
函数的返回类型也是int
,它返回读取的字符的ASCII码值,如果到达文件结尾或出现错误,则返回EOF。
-
用途不同:
getchar
通常用于从标准输入流(stdin)中读取用户输入,例如从控制台读取字符。fgetc
通常用于从文件中读取字符,通过指定文件流来实现对文件的逐个字符读取。
-
可移植性:
getchar
函数是标准C库函数,因此具有很好的可移植性,可以在不同平台上使用。fgetc
函数也是标准C库函数,但在处理文件时更加灵活,可以指定不同的文件流来读取字符。
总的来说,
getchar()
是专门用于标准输入流的字符读取函数,而fgetc()
可以用于任何输入流的字符,包括文件流等。两者都会移动读取位置到下一个字符,但是fgetc()
更加通用。 -
13.文件操作常用的函数
1.fgetc函数
fgetc函数是C语言中的一个标准库函数,源自C标准I/O库,用于从指定的文件中读取一个字符(1个字节)。其语法格式为int fgetc(FILE * fp);
,其中参数fp
为要读取的文件的文件指针变量。
当fgetc函数读取成功时,它会返回读取到的字符的ASCII码值;如果读取到文件末尾或读取失败,则返回EOF(End Of File的缩写,表示文件末尾)。EOF是在stdio.h中定义的宏,其值通常是一个负数,如-1。
此外,fgetc函数不仅可以用来读取文件中的字符,还可以用来读取来自标准输入流的字符,只需将标准输入流标志stdin作为文件指针fp即可。同时,它还可以跳过指定字符,如空白字符,使得文件读取变得更加方便快捷。
然而,fgetc函数的一个主要缺点是它不能按行读取文件,每次只能读取一个字符。如果需要实现按行读取的功能,可能需要结合其他函数或方法来完成。
在使用fgetc函数时,还需要注意进行错误处理。例如,可以使用FEOF函数进行文件结束检测,以及使用error函数进行出错检查。
fgetc成功时返回读到一个字节,本来应该是unsigned char型的,但由于函数原型中返回值是int型,所以这个字节要转换成int型再返回。
系统对于每个打开的文件都记录着当前读写位置在文件的地址(或者说距离文件开头的字节数),也叫偏移量(offset)。
要用fgetc函数读一个文件,该文件的打开方式必须是可读的。
总的来说,fgetc函数是C语言中用于文件操作的重要函数之一,它提供了一种从文件中逐个字符读取数据的方式。
2.fgets函数
fgets
是 C 语言中的一个标准库函数,用于从指定的流中读取一行数据,并将其保存到提供的字符数组中。它的基本用法如下:
函数原型
char *fgets(char *str, int n, FILE *stream);
参数
str
:一个字符数组,用于存储读取到的字符串。n
:一个整数,指定要读取的最大字符数(包括末尾的空字符 ‘\0’)。,fgets函数自动添加\0,将输入的变为字符串,终断输入的并不包含/0,/0不属于文件本身内容stream
:一个指向FILE
对象的指针,代表要从中读取数据的输入流。
返回值
- 成功时,返回指向存储读取数据的字符数组的指针(通常是
str
)。 - 失败时(如读取到文件末尾或发生错误),返回
NULL
。
注意事项
fgets
会读取直到遇到换行符(\n
)、EOF 或读取了n-1
个字符为止(为末尾的空字符 ‘\0’ 留出空间)。- 如果在读取的
n-1
个字符之内遇到换行符,fgets
会将换行符也保存到str
中,并在之后添加空字符 ‘\0’。 - 如果没有遇到换行符,而是在读取了
n-1
个字符后遇到 EOF 或文件结束,fgets
也会在这些字符后添加空字符 ‘\0’。
示例
下面是一个使用 fgets
从文件中读取一行的简单示例:
#include <stdio.h>
int main() {
FILE *file = fopen("example.txt", "r");
if (file == NULL) {
perror("Error opening file");
return 1;
}
char line[100]; // 足够大的字符数组来存储一行数据
while (fgets(line, sizeof(line), file)) {
// 打印读取到的行,不包括末尾的换行符(如果有的话)
printf("%s", line);
}
fclose(file);
return 0;
}
在这个示例中,fgets
被用于从 example.txt
文件中逐行读取数据,并打印到控制台上。sizeof(line)
用于确定 line
数组的大小,这样 fgets
就不会尝试读取超过数组容量的数据,从而避免了缓冲区溢出的风险。
记住,在使用 fgets
或其他文件操作函数后,应该检查返回值以确认操作是否成功,并始终在使用完毕后关闭文件。
3.fputs函数
在C语言中,fputs
函数用于将字符串写入指定的文件流。其基本语法如下:
int fputs(const char *str, FILE *stream);
str
是一个指向要写入文件的字符串的指针。stream
是指向FILE
对象的指针,该对象标识了要写入的文件流。
fputs
函数将 str
指向的字符串写入到 stream
指向的文件中,直到遇到字符串末尾的空字符(\0
)为止。如果成功,fputs
函数返回非负值;如果发生错误,则返回 EOF
。
以下是一个使用 fputs
函数将字符串写入文件的简单示例:
#include <stdio.h>
int main() {
FILE *fp;
char str[] = "Hello, World!";
// 打开文件,以写入模式("w")
fp = fopen("output.txt", "w");
if (fp == NULL) {
perror("Error opening file");
return 1;
}
// 使用 fputs 将字符串写入文件
if (fputs(str, fp) == EOF) {
perror("Error writing to file");
fclose(fp);
return 1;
}
// 关闭文件
fclose(fp);
return 0;
}
在上面的示例中,我们首先打开一个名为 output.txt
的文件以进行写入。然后,我们使用 fputs
函数将字符串 "Hello, World!"
写入文件。如果 fputs
返回 EOF
,则表示写入过程中发生了错误。最后,我们关闭文件。
需要注意的是,fputs
不会在写入的字符串后面自动添加换行符。如果需要换行,你需要在写入的字符串中包含换行符(\n
),或者在调用 fputs
后使用 fputc('\n', fp)
单独写入一个换行符。同时,也要确保在调用 fputs
之前已经正确打开了文件,并且在操作完成后关闭文件。
4.gets函数
gets
函数是C语言中的一个函数,它属于C标准库<stdio.h>
。gets
函数的主要功能是读取整行输入,直到遇到换行符为止,然后丢弃换行符,将剩余的字符存储在提供的字符数组中,并在字符串末尾添加一个空字符(\0
),使其成为合法的C字符串。
函数原型为:
char *gets(char *str);
这里,str
是一个指向字符数组的指针,该数组用于存储从输入流中读取的字符串。
然而,值得注意的是,gets
函数在使用上存在一些严重的安全问题。具体来说,当gets
函数的参数是一个有限大小的字符数组时,它无法检查数组的空间是否足够存储所有输入行。如果输入的字符串过长,会导致缓冲区溢出,多余的字符会超出指定的目标空间,这可能会导致程序崩溃或更严重的安全问题。因此,在现代C语言编程中,通常推荐使用fgets
函数来替代gets
函数,因为fgets
允许指定读取的最大字符数,从而避免缓冲区溢出的问题。
下面是一个简单的gets
函数使用示例:
#include <stdio.h>
int main() {
char str[50]; // 定义一个足够大的字符数组来存储输入
printf("请输入一个字符串:");
gets(str); // 使用gets函数读取字符串
printf("你输入的字符串是:%s\n", str); // 输出读取到的字符串
return 0;
}
在这个示例中,用户被提示输入一个字符串,然后使用gets
函数读取这个字符串,并将其存储在str
数组中。最后,程序输出读取到的字符串。然而,由于gets
函数的不安全性,这个示例在实际编程中并不推荐。
总的来说,尽管gets
函数在某些情况下可能很方便,但由于其存在的安全问题,建议使用更安全的替代函数,如fgets
。
5.fread函数
fread
是 C 语言标准库中的一个函数,用于从文件流中读取数据。它通常用于二进制文件的读取,但也可以用于文本文件。fread
函数的基本语法如下:
size_t fread(void *ptr, size_t size, size_t count, FILE *stream);
参数说明:
ptr
:指向一个数据块的指针,该数据块用于存储从文件中读取的数据。size
:要读取的每个数据项的大小(以字节为单位)。count
:要读取的数据项的数量。stream
:指向FILE
对象的指针,该对象标识了要进行读取操作的文件流。
fread
函数返回成功读取的数据项数量,如果发生错误或到达文件末尾,则返回的数量可能小于 count
。
下面是一个使用 fread
读取二进制文件的简单示例:
#include <stdio.h>
int main() {
FILE *file = fopen("data.bin", "rb"); // 打开二进制文件用于读取
if (file == NULL) {
perror("Error opening file");
return 1;
}
// 假设我们知道文件包含整数,每个整数占用 sizeof(int) 字节
int buffer[10]; // 创建一个整数数组来存储读取的数据
size_t itemsRead = fread(buffer, sizeof(int), 10, file); // 尝试读取 10 个整数
if (ferror(file)) { // 检查是否有读取错误
perror("Error reading file");
fclose(file);
return 1;
}
// 输出读取到的整数
for (size_t i = 0; i < itemsRead; ++i) {
printf("%d\n", buffer[i]);
}
fclose(file); // 关闭文件
return 0;
}
在上面的示例中,我们打开了一个名为 “data.bin” 的二进制文件,并尝试从中读取 10 个整数。如果读取成功,我们将读取到的整数输出到控制台。
注意,fread
不会为读取的数据添加任何终止字符(如文本文件中的换行符),它仅按请求的字节数读取数据。因此,如果处理的是文本文件,并且需要以某种方式区分字符串的结束(例如,在 C 风格的字符串中需要一个空字符 '\0'
),则需要自己处理这个问题。
另外,使用 fread
读取文本文件时,必须注意编码问题,因为 fread
读取的是原始字节,而不是字符。如果文本文件使用的是多字节字符编码(如 UTF-8),则每个字符可能占用多个字节,这会影响你如何解释和处理读取到的数据。
二进制文件
二进制文件时内存的映像
写成2进制文件
#include <stdio.h>
#include <stdlib.h>
typedef struct student
{
int ID;
char name[20];
char sex;
int age;
}student;
int main(void)
{
FILE *fp;
student s[2] = {{1, "Zhangpengbo", 'm', 19},{2, "Weishitong", 'f', 17}};
if ( (fp = fopen("9.dat", "w")) == NULL) {
perror("Open 9.dat\n");
exit(1);
}
fwrite(s, sizeof(student), 2, fp);
fclose(fp);
return 0;
}
读取二进制文件
#include <stdio.h>
#include <stdlib.h>
typedef struct student
{
int ID;
char name[20];
char sex;
int age;
}student;
int main(void)
{
FILE *fp;
student s[2];
if ( (fp = fopen("9.dat", "r")) == NULL) {
perror("Open 9.dat\n");
exit(1);
}
fread(s, sizeof(student), 2, fp);
printf("%d %s %c %d\n", s[0].ID,s[0].name, s[0].sex, s[0].age);
printf("%d %s %c %d\n", s[1].ID,s[1].name, s[1].sex, s[1].age);
fclose(fp);
return 0;
}
文本文件按格式修改,并不会影响输出,按行输出,行为单位
文本文件和二进制文件是计算机中两种常见的文件类型,它们在存储和处理数据时有着显著的不同。
-
文本文件:
- 内容:文本文件主要存储文本信息,即人类可读的字符数据。这些字符数据通常以某种字符编码(如ASCII、UTF-8等)存储。
- 用途:文本文件常用于存储和传输文本信息,如源代码、文档、网页内容等。
- 处理:由于文本文件的内容是人类可读的,因此它们通常可以直接用文本编辑器打开和编辑。此外,各种编程语言也提供了丰富的文本处理功能,可以方便地读取、写入和修改文本文件。
- 跨平台性:由于文本文件的内容是字符数据,因此它们通常具有较好的跨平台性。只要字符编码一致,不同操作系统和应用程序通常都能正确读取和处理文本文件。
-
二进制文件:
- 内容:二进制文件存储的是二进制数据,即0和1的序列。这些数据可能表示各种类型的信息,如图像、音频、视频、程序等。
- 用途:二进制文件通常用于存储那些不适合用文本表示的数据,如多媒体内容、可执行程序等。
- 处理:由于二进制文件的内容是机器码,因此通常需要特定的程序或库来解析和处理。直接打开二进制文件通常看到的是乱码或无意义的字符。
- 跨平台性:二进制文件的跨平台性较差。因为不同操作系统和处理器架构的二进制格式可能不同,所以一个平台上生成的二进制文件可能无法在另一个平台上直接运行。然而,通过一些技术手段(如跨平台编译、虚拟机等),可以实现二进制文件在不同平台上的运行。
总结来说,文本文件和二进制文件在内容、用途、处理和跨平台性等方面都有着明显的区别。选择使用哪种类型的文件取决于具体的应用场景和需求。
6.sprintf函数
在C语言中,将数字转化为字符串是一个常见的操作。你可以使用多种方法来实现这一转换,但通常最直接的方法是使用sprintf
函数。sprintf
函数是C标准库中的一个函数,用于将格式化的数据写入字符串。
下面是一个使用sprintf
将整数转化为字符串的示例:
#include <stdio.h>
int main() {
int number = 12345;
char str[50]; // 确保字符串数组足够大以容纳转换后的数字以及空字符'\0'
// 使用sprintf将整数转换为字符串
sprintf(str, "%d", number);
// 输出转换后的字符串
printf("The number as a string is: %s\n", str);
return 0;
}
在这个例子中,%d
是格式说明符,它告诉sprintf
函数我们希望将一个整数插入到字符串中。sprintf
函数会将number
的值转换为字符串,并将结果存储在str
数组中。
对于浮点数,你可以使用%f
格式说明符,如下所示:
#include <stdio.h>
int main() {
double number = 3.14159;
char str[50]; // 确保字符串数组足够大以容纳转换后的数字以及空字符'\0'
// 使用sprintf将浮点数转换为字符串,保留两位小数
sprintf(str, "%.2f", number);
// 输出转换后的字符串
printf("The number as a string is: %s\n", str);
return 0;
}
在这个例子中,%.2f
告诉sprintf
函数我们希望将浮点数转换为字符串,并保留两位小数。
注意,使用sprintf
时,你需要确保目标字符串数组有足够的空间来容纳转换后的字符串以及结尾的空字符\0
。否则,如果数组太小,就会发生缓冲区溢出,这可能会导致程序崩溃或产生不可预测的行为。
如果你不确定需要多大的数组,可以使用snprintf
函数,它允许你指定一个最大字符数限制,以防止缓冲区溢出:
#include <stdio.h>
int main() {
int number = 12345;
char str[50]; // 假设这个数组足够大
int max_chars = sizeof(str) - 1; // 减去1以保留空字符'\0'的空间
// 使用snprintf将整数转换为字符串,并确保不会超出str的大小
snprintf(str, max_chars, "%d", number);
// 输出转换后的字符串
printf("The number as a string is: %s\n", str);
return 0;
}
在这个例子中,snprintf
确保不会向str
写入超过max_chars
个字符(包括结尾的空字符\0
)。这有助于防止缓冲区溢出。
7.snprintf函数
snprintf
是 C 语言中用于格式化输出字符串到指定大小缓冲区的函数。它类似于 printf
,但提供了更多的控制和安全性,因为它可以防止缓冲区溢出。
snprintf
函数的原型如下:
int snprintf(char *str, size_t size, const char *format, ...);
参数说明:
str
:指向目标字符串的指针,即格式化后的结果将被存储在这里。size
:目标字符串的最大容量,包括结尾的空字符\0
。format
:格式化字符串,包含输出的格式和占位符。...
:可变数量的参数,用于填充format
中的占位符。
snprintf
函数将格式化的字符串写入 str
指向的缓冲区,但最多写入 size - 1
个字符,然后在末尾添加一个空字符 \0
以标记字符串的结束。如果格式化后的字符串长度超过 size - 1
,则结果会被截断。
snprintf
函数的返回值是输出到 str
缓冲区中的字符数(不包括字符串结尾的空字符 \0
)。这个返回值可以用于检查是否发生了截断。
下面是一个使用 snprintf
的示例:
#include <stdio.h>
int main() {
char buffer[20];
int number = 123;
float value = 45.67f;
// 使用 snprintf 将格式化的字符串写入 buffer
int result = snprintf(buffer, sizeof(buffer), "Number: %d, Value: %.2f", number, value);
// 打印结果和返回的字符数
printf("Formatted string: '%s'\n", buffer);
printf("Number of characters written (excluding null terminator): %d\n", result);
return 0;
}
在这个示例中,snprintf
将格式化的字符串写入 buffer
,它的大小为 20 个字符。如果格式化的字符串超过 19 个字符(需要留一个位置给空字符 \0
),则结果将被截断。snprintf
还会返回实际写入的字符数,不包括空字符。
使用 snprintf
相比 sprintf
更安全,因为 sprintf
不会检查目标缓冲区的大小,这可能导致缓冲区溢出。而 snprintf
通过限制写入的字符数来防止这种情况发生。
8.fprintf函数
fprintf
是 C 语言中的一个标准库函数,用于将格式化的输出写入到指定的文件流中。它类似于 printf
,但 printf
将输出到标准输出(通常是终端或控制台),而 fprintf
则允许你指定一个文件作为输出目标。
fprintf
函数的原型如下:
int fprintf(FILE *stream, const char *format, ...);
参数说明:
stream
:指向要写入的FILE
对象的指针,该对象标识了目标文件流。format
:一个格式字符串,它指定了如何格式化后续参数以及它们的输出。...
:可变参数列表,这些参数根据format
字符串中的格式说明符进行格式化并写入文件。
fprintf
函数返回写入的字符数(不包括结尾的空字符),如果发生错误则返回负值。
下面是一个使用 fprintf
函数的简单示例:
#include <stdio.h>
int main() {
FILE *file = fopen("output.txt", "w"); // 打开文件用于写入
if (file == NULL) {
perror("Error opening file");
return 1;
}
int number = 42;
float value = 3.14f;
// 使用 fprintf 将格式化的字符串写入文件
fprintf(file, "The number is: %d\n", number);
fprintf(file, "The value is: %.2f\n", value);
fclose(file); // 关闭文件
return 0;
}
在上面的代码中,我们打开一个名为 output.txt
的文件,并使用 fprintf
将两个格式化的字符串写入该文件。第一个字符串包含一个整数,第二个字符串包含一个浮点数,并保留两位小数。完成写入后,我们关闭了文件。如果一切顺利,现在 output.txt
文件应该包含两行文本,分别显示整数和浮点数的值。
使用 fprintf
可以很容易地将数据格式化并输出到文件,这对于记录日志、生成报告或保存程序数据等场景非常有用。记得在使用完文件后调用 fclose
来关闭文件,以确保所有的数据都被正确地写入并保存。
9.fscanf函数
fscanf
是 C 语言中的一个标准库函数,用于从文件读取格式化输入。它位于标准输入输出库 <stdio.h>
中。fscanf
函数的基本语法如下:
int fscanf(FILE *stream, const char *format, ...);
这里:
stream
是一个指向FILE
类型的指针,它指定了要读取数据的文件或流。format
是一个字符串,包含了要读取数据的格式说明符。格式说明符定义了应该如何读取接下来的输入。...
是可变数量的参数,对应于format
中的格式说明符,用来存储读取的数据。
fscanf
根据 format
指定的格式从 stream
指向的文件或流中读取数据,并将读取的数据存储在提供的变量中。当遇到空格、换行或达到指定格式的结束位置时,fscanf
会停止读取。
以下是一个简单的示例,演示了如何使用 fscanf
函数从文件中读取两个整数:
#include <stdio.h>
int main() {
FILE *file;
int num1, num2;
// 打开文件
file = fopen("data.txt", "r");
if (file == NULL) {
perror("Error opening file");
return 1;
}
// 从文件中读取两个整数
if (fscanf(file, "%d %d", &num1, &num2) != 2) {
printf("Failed to read two integers from the file.\n");
} else {
printf("num1: %d\n", num1);
printf("num2: %d\n", num2);
}
// 关闭文件
fclose(file);
return 0;
}
在这个例子中,我们首先打开一个名为 “data.txt” 的文件,并使用 fscanf
读取两个整数。如果 fscanf
返回值不等于 2,表示没有成功读取两个整数。然后,我们输出读取到的整数,并关闭文件。
需要注意的是,当使用 fscanf
时,要确保文件以正确的模式打开(如读取模式),格式字符串与文件中的数据格式相匹配,文件指针位置正确,以及文件尚未结束。如果 fscanf
函数没有读取到任何东西,你应该检查这些因素。
fscanf
函数的格式化字符串可以包含普通字符和格式说明符。常见的格式说明符包括:
%d
:读取一个整数。%f
:读取一个浮点数。%c
:读取一个字符。%s
:读取一个字符串。
根据具体的格式说明符,你需要传递相应类型的变量地址给 fscanf
函数,以便正确存储读取的数据。
10.rewind函数
rewind函数是C语言中的一个标准库函数,它的主要作用是将文件内部的位置指针重新指向一个流(数据流/文件)的开头。换句话说,它用于将读写位置指针重置到文件的起始处。这意味着,在调用rewind函数后,下一次对文件的读取或写入操作将从文件的开头开始。
rewind函数的语法如下:
void rewind(FILE *stream);
其中,stream
是一个指向FILE
类型的指针,它指向已打开的文件。在使用rewind函数之前,必须通过fopen
函数打开文件并返回一个指向该文件的FILE
类型指针。
值得注意的是,rewind函数没有返回值,因此无法直接判断该操作是否成功。但是,在实际使用中,我们可以通过ferror
和clearerr
等相关函数来判断是否发生了错误。
例如:
FILE *file = fopen("example.txt", "r");
if (file != NULL) {
// 对文件进行一系列操作...
rewind(file); // 将文件指针重置到文件开头
// 从文件开头开始新的读取或写入操作...
fclose(file); // 关闭文件
}
在上面的例子中,我们首先使用fopen
函数打开一个名为example.txt
的文件进行读取。然后,在对文件进行一系列操作后,我们使用rewind函数将文件指针重置到文件的开头,以便从文件的起始位置开始新的读取或写入操作。最后,我们使用fclose
函数关闭文件。
总之,rewind函数在C语言文件操作中非常有用,它允许我们方便地重新定位文件指针到文件的开头,从而实现对文件的重新读取或写入。
11.fseek函数
fseek函数是C语言中的一个文件操作函数,用于改变文件的当前读写位置。它允许你定位文件操作的指针位置,使得文件读写操作可以从指定的位置开始。
函数的原型如下:
int fseek(FILE *stream, long offset, int origin);
其中,参数的含义如下:
stream
:文件指针,它指向一个已经打开的文件。offset
:偏移量,表示要移动的字节数,可以是正数或负数。正数表示向前移动,负数表示向后移动。origin
:起始位置,用于确定offset
的参考点,它有三个可能的取值:SEEK_SET
:从文件开头开始计算偏移量。SEEK_CUR
:从当前位置开始计算偏移量。SEEK_END
:从文件末尾开始计算偏移量。
fseek函数的返回值是一个整数。如果成功,它返回0;如果失败(例如,由于文件未打开或发生其他错误),则返回非零值。
fseek函数在多种场景下都非常有用,例如:
- 在读写大文件时,需要对文件进行分段处理,使用fseek函数可以快速定位到目标位置。
- 修改已有的文本文件内容时,需要先将指针移到修改点处后再进行修改。
- 将文件指针重置到起始位置以便重新读取文件。
以下是一个简单的示例,展示如何使用fseek函数将文件指针定位到文件的第5个字符处:
FILE *fp = fopen("example.txt", "r"); // 打开文件
if (fp == NULL) {
// 处理文件打开失败的情况
}
fseek(fp, 4, SEEK_CUR); // 将文件指针定位到第5个字符处(从当前位置向前移动4个字节)
char ch = fgetc(fp); // 读取文件指针当前位置的字符并输出
printf("%c", ch);
fclose(fp); // 关闭文件
请注意,在使用fseek函数之前,需要先打开一个文件,并将文件指针赋值给相应的变量。此外,调用fseek函数时,需要指定文件的打开方式(如文本模式或二进制模式),以便正确处理文件中的数据。
12.ftell函数
ftell函数是C语言中的一个标准库函数,定义在<stdio.h>头文件中,用于获取文件指针的当前偏移量,也就是从文件开头到当前位置的字节数量。其函数原型为long ftell(FILE *stream)
,其中FILE *stream
是目标文件的文件指针。
ftell函数在文件操作中有着广泛的应用。例如,当需要确定文件的读写位置时,可以使用ftell函数来获取当前文件指针的位置。另外,当想要获取文件的大小时,可以先使用ftell函数获取当前文件的位置,然后使用fseek函数将文件指针移动到文件末尾,再调用ftell函数来查看文件大小。
需要注意的是,ftell函数的返回值是以字节为单位的长整型数值。如果调用成功,则返回当前文件指针相对于文件起始位置的偏移量;如果调用失败,则返回-1,并会设置errno以指示错误原因。
用文件操作,偏移量打印文件字符长度
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
FILE *fp;
int ch;
if ( (fp = fopen("6.c", "r+")) == NULL) {
perror("Open 6.txt\n");
exit(1);
}
fseek(fp, 0, SEEK_END);
int len = ftell(fp);
fputs("helloworld\n",fp);//给6.c末尾追加字符
printf("%d\n", len);
fclose(fp);
return 0;
}
14.字符串
-
字符串可以看作一个数组,它的每个元素是字符型的,字符串变量
-
char ***str = “Hello”;**存放在rodate段,rodate段一般用于存放常量,只读不改。存放在rodata段的被称为字符串常量,只读,无法修改。
-
atoi函数
atoi
函数是一个常见的函数,用于将字符串转换为整数。
1. strlen函数
- 原型:
unsigned int strlen(const char * str)
- 作用:计算str的长度并返回
- 源码:
#include <stdio.h>
size_t my_strlen(const char * str)
{
unsigned len = 0;
while(*str != '\0')
{
len++;
str++;
}
return len;
}
int main()
{
char * str = "abcdefgh";
int len = 0;
len = my_strlen(str);
printf("%d\n", len);
return 0;
}
2、strcpy函数与strncpy函数
strcpy函数
- 原型:
char *strcpy(char * dest, const char * src)
- 作用:把src指向的字符串拷贝到dest指向的字符串中
- 源码:
#include <stdio.h>
#include <assert.h>
#include <string.h>
char *my_strcpy(char *dest, const char *src)
{
assert((dest != NULL) && (src != NULL));
char *o_dest = dest;
while (*src != '\0')
{
*dest++ = *src++;
}
*dest = '\0';
return o_dest;
}
int main()
{
char dest[10] = {0};
char *src = "abcdefgh";
my_strcpy(dest, src);
printf("%s\n", dest);
return 0;
}
- strncpy函数
- 原型:
char *strncpy(char *dest, char *src, int n)
- 作用:把str2指向的前count个字符拷贝到str1指向的字符串中
- 源码:
#include <stdio.h>
#include <string.h>
#include <assert.h>
char *my_strncpy(char *dest, const char *src, size_t n)
{
size_t i;
for (i = 0; i < n && src[i] != '\0'; i++)
{
dest[i] = src[i];
}
for ( ; i < n; i++)
{
dest[i] = '\0';
}
return dest;
}
int main()
{
char dest[10] = {0};
char *src = "abcdefgh";
int n = 5;
my_strncpy(dest, src, n);
printf("%s\n", dest);
return 0;
}
3、strcmp函数
- 原型:
int strcmp(const char *s1, const char *s2)
- 作用:比较str1和str2,str1 > str2返回1,str1 == str2返回0
- 源码:
#include <stdio.h>
#include <assert.h>
int my_strcmp(const char *s1, const char *s2)
{
assert(s1 != NULL && s2 != NULL);
while (*s1 != '\0')
{
if (*s1 != *s2)
{
break;
}
s1++;
s2++;
}
return *s1 - *s2;
}
int main()
{
char *s1 = "aaaaa";
char *s2 = "aaaab";
char *s3 = "aaaac";
int ret = 0;
ret = my_strcmp(s2, s1);
if (ret > 0)
{
printf("string 2 is bigger than string 1\n");
}
else
{
printf("string 2 is smaller than string 1\n");
}
ret = my_strcmp(s2, s3);
if (ret > 0)
{
printf("string 2 is bigger than string 3\n");
}
else
{
printf("string 2 is smaller than string 3\n");
}
return 0;
}
4、strncmp函数
- 原型:
int strncmp(const char * s1, const char * s2, int n)
- 作用:比较str1和str2的前n个字符
- 源码:
#include <stdio.h>
#include <assert.h>
int my_strncmp(const char * s1, const char * s2, int n)
{
assert(s1 != NULL &&s2 != NULL & n > 0);
while(--n && *s1 != 0)
{
if (*s1 != *s2)
{
break;
}
s1++;
s2++;
}
return *s1 - *s2;
}
int main()
{
char *str1 = "China is a nation!";
char *str2 = "French is a nation!";
int count = 5;
int ret = 0;
ret = my_strncmp(str1, str2, count);
if(ret != 0)
printf("str1 is not equal to str2!\n");
return 0;
}
5、strcasecmp、strncasecmp与stricmp函数
- 原型:
#include <strings.h>
int strcasecmp(const char *s1, const char *s2);
int strncasecmp(const char *s1, const char *s2, size_t n);
int stricmp(const char *s1, const char *s2);
备注 :仅在windows环境中使用- 作用:不区分大小写的比较str1和str2
- 源码:
#include <stdio.h>
#include <strings.h>
int my_strcasecmp(const char *s1, const char *s2)
{
char ch1;
char ch2;
while(*s1 != '\0')
{
ch1 = *s1++;
if(ch1 >= 'A' && ch1 <= 'Z')
{
ch1 += 'a' - 'A';
}
ch2 = *s2++;
if(ch2 >= 'A' && ch2 <= 'Z')
{
ch2 += 'a' - 'A';
}
if (ch1 != ch2)
{
break;
}
}
if (ch1 == ch2)
{
return 0;
}else if (ch1 > ch2)
{
return 1;
}else
{
return -1;
}
}
int main()
{
char *s1= "ammana";
char *s2 = "bibi";
char *s3 = "AMMANA";
int ret1 = 0;
int ret2 = 0;
ret1 = my_strcasecmp(s1, s2);
if(ret1 > 0)
{
printf("str1 bigger than str2!\n");
}
else
{
printf("str1 smaller than str2!\n");
}
ret2 = my_strcasecmp(s1, s3);
if(ret2 > 0)
{
printf("str1 bigger than str3!\n");
}
else if(ret2 < 0)
{
printf("str1 smaller than str3!\n");
}
else
{
printf("str1 equal to str3!\n");
}
return 0;
}
6、strcat函数
- 原型:
char *strcat(char *dest, const char *src)
- 作用:连接两个字符串,将src连接到dest
- 源码:
#include <stdio.h>
char * my_strcat(char* dest, const char *src)
{
char * dest_o = dest;
while(*dest != '\0')
{
dest++;
}
while(*src != '\0')
{
*dest++ = *src++;
}
*dest = '\0';
return dest_o;
}
int main()
{
char dest[25];
char *space = " ";
char *s1 = "Hello";
char *s2 = "world!";
strcpy(dest, s1);
my_strcat(dest, space);
my_strcat(dest, s2);
printf("%s\n", dest);
return 0;
}
7、strncat函数
- 原型:
char *strncat(char *dest, const char *src, size_t n);
- 作用:连接两个字符串,将src连接到dest
- 源码:
#include <stdio.h>
#include <assert.h>
char *my_strncat(char *dest, const char *src, int n)
{
assert((dest != NULL) && (src != NULL));
char *dest_o = dest;
while (*dest != '\0')
{
dest++;
}
while (n-- && *src != '\0' )
{
*dest++ = *src++;
}
*dest = '\0';
return dest_o;
}
int main()
{
char dest[25];
char *space = " ";
char *s1 = "Hello";
char *s2 = "world!";
strcpy(dest, s1);
my_strncat(dest, space, 4);
my_strncat(dest, s2, 5);
printf("%s\n", dest);
return 0;
}
8、strchr函数
- 原型:
char * strchr(char * str, const char c)
- 作用:查找str中c首次出现的位置,并返回位置或者NULL
- 源码:
#include <stdio.h>
char * my_strchr(char *str, const char c)
{
while(*str != '\0')
{
if (*str == c)
{
return str;
}
str++;
}
return NULL;
}
int main()
{
char *s1 = "abcdefgh";
char c = 'd';
char *ptr = NULL;
ptr = my_strchr(s1, c);
if(ptr != NULL)
{
printf("%c %c\n", *ptr, c);
}
else
{
printf("Not Found!\n");
}
return 0;
}
9、strrchr函数
- 原型:
char * strrchr(char * str, const char c)
- 作用:查找str中c最后一次出现的位置,并返回位置或者NULL
- 源码:
#include <stdio.h>
#include <string.h>
char * my_strrchr(char * str, const char c)
{
char * end = str + strlen(str);
while(str != end)
{
if (*end == c)
{
return end;
}
end--; //向前进行移动
}
return NULL;
}
int main()
{
char * string = "abcdefdefgh";
char c = 'd';
char *p = NULL;
p = my_strrchr(string, c);
if(p != NULL)
{
printf("%s\n", p);
}
else
{
printf("Not Found!\n");
}
return 0;
}
10、strrev函数
- 原型:
char * strrev(char * str)
- 作用:翻转字符串并返回字符串指针
- 源码:
#include <stdio.h>
#include <string.h>
#include <assert.h>
char *my_strrev(char * str)
{
assert(str != NULL);
char *end = str;
char *head = str;
char tmp;
while (*end != '\0')
{
end++;
}
end--; /* 回跳过结束符'\0' */
/* 当head和end未重合时,交换它们所指向的字符 */
while (head < end)
{
tmp = *head;
*head= *end; /* head向尾部移动 */
*end= tmp; /* end向头部移动 */
head++;
end--;
}
return str;
}
#if 0
/*第二种实现方式*/
char * my_strrev(char * str)
{
assert(str != NULL);
char *end = strlen(str) + str - 1;
char temp = '0';
while(str < end)
{
temp = *str;
*str = *end;
*end = temp;
str++;
end--;
}
return str;
}
#endif
int main()
{
char str[] = "Hello World";
printf("Before reversal: %s\n", str);
my_strrev(str);
printf("After reversal: %s\n", str);
return 0;
}
11、strdup函数
- 原型:
char * strdup(const char * str)
- 作用:拷贝字符串到新申请的内存中返回内存指针,否则返回NULL
- 源码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
char * my_strdup(const char * str)
{
assert(str != NULL);
char * str_t = (char *)malloc(strlen(str) + 1);
if (str_t == NULL)
{
printf("malloc failed\n");
exit(1);
}
strcpy(str_t, str); //进行拷贝
return str_t; //返回的内存在堆中需要手动释放内存
}
int main()
{
char *str1 = "abcde";
char *str2 = NULL;
str2 = my_strdup(str1);
printf("%s\n", str2);
free(str2); //释放内存
return 0;
}
12、strstr函数
- 原型:
char * my_strstr(const char * haystack, char * needle)
- 作用:查找str2在str1中出现的位置,找到返回位置,否则返回NULL
- 源码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
char * my_strstr(const char * haystack, char * needle)
{
assert(haystack != NULL & needle != NULL);
int len1 = strlen(haystack);
int len2 = strlen(needle);
while(len1 >= len2)
{
len1--;
if(strncmp(haystack, needle, len2) == 0)
{
return haystack;
}
haystack++;
}
return NULL;
}
int main()
{
char *str1 = "China is a nation, a great nation!";
char *str2 = "nation";
char *p = NULL;
p = my_strstr(str1, str2);
printf("The string is: %s\n", p);
return 0;
}
13、strpbrk函数
- 原型:
char *strpbrk(const char *str1, const char *str2)
- 作用:从str1的第一个字符向后检索,直到’\0’,如果当前字符存在于str2中,那么返回当前字符的地址,并停止检索.
- 源码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
/*strpbrk是在源字符串(s1)中找出最先含有搜索字符串(s2)中任一字符的位置并返回,若找不到则返回空指针。*/
char *my_strpbrk(const char *s, const char *accept)
{
assert((s != NULL) && (accept != NULL));
const char *t;
while (*s != '\0')
{
t = accept;
while (*t != '\0')
{
if (*s == *t)
{
return (char *) s;
}
++t;
}
++s;
}
return NULL;
}
int main()
{
char s1[] = "www.baidu.com";
char s2[] = "esu";
char *p = my_strpbrk(s1, s2);
if(p != NULL)
{
printf("The result is: %s\n",p);
}
else
{
printf("Sorry!\n");
}
return 0;
}
14、strspn函数
- 原型:
size_t strspn(const char *s, const char *accept);
- 作用:检索字符串 s 中第一个不在字符串 accpet 中出现的字符下标。
- 源码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
/*检索字符串 s 中第一个不在字符串 accept 中出现的字符下标。*/
int my_strspn(const char *s, const char *accept)
{
assert((s != NULL) && (accept != NULL));
const char *t;
const char *o_s = s;
int count = 0;
while (*s != '\0')
{
t = accept;
while (*t != '\0')
{
if (*s == *t)
{
break;
}
++t;
}
if (*t == '\0')
{
return s - o_s;
}
s++;
}
return 0;
}
int main ()
{
int len;
const char str1[] = "ss1ABCDEFGAA019874";
const char str2[] = "s1BA";
len = my_strspn(str1, str2);
printf("str1 %d\n", len );
len = strspn(str1, str2);
printf("str1 %d\n", len );
return 0;
}
15、strcspn函数
- 原型:
size_t strcspn(const char *s, const char *reject);
- 作用:从参数s 字符串的开头计算连续的字符,而这些字符都完全不在参数reject 所指的字符串中。
- 若strcspn()返回的数值为n,则代表字符串s 开头连续有n 个字符都不含字符串reject 内的字符。 返回值:返回字符串s 开头连续不含字符串reject 内的字符数目。
- 源码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
size_t my_strcspn(const char *str1, const char *reject)
{
assert((str1 != NULL) && (reject != NULL));
const char *t;
const char *s = str1;
while (*s != '\0')
{
t = reject;
while (*t != '\0')
{
if (*s == *t)
{
return s - str1;
}
++t;
}
++s;
}
return 0;
}
int main()
{
int len;
const char str1[] = "ABECDEFGHI123456780";
const char str2[] = "EF";
len = my_strcspn(str1, str2);
printf("%d\n", len);
len = strcspn(str1, str2);
printf("%d\n", len);
return(0);
}
16、strset 与 strnset函数
- 原型:
char *strset(char *str, int ch)
#include <stdio.h>
#include <assert.h>
//功能:strset把字符串s中的所有字符都设置成字符ch的函数。
char *strset(char *str, int ch)
{
assert(str != NULL);
char *s = str;
while (*s != '\0')
{
*s = (char)ch;
s++;
}
return str;
}
//功能:strnset将一个字符串中的前n个字符都设为指定字符ch的函数。
char *strnset(char *str, int ch, int n)
{
assert(str != NULL);
char *s = str;
while (*s != '\0' && s - str < n)
{
*s = (char)ch;
++s;
}
return str;
}
int main()
{
char dest[25] = "Helloworld";
strnset(dest, 'a', 5);
printf("%s\n", dest);
strset(dest, 'a');
printf("%s\n", dest);
return 0;
}
17、strtok函数
- 原型:
char *strtok(char *s, const char *delim)
- 作用:
- 源码:
#include <stdio.h>
#include <string.h>
#include <assert.h>
/* Parse S into tokens separated by characters in DELIM.
If S is NULL, the saved pointer in SAVE_PTR is used as
the next starting point. For example:
char s[] = "-abc-=-def";
char *sp;
x = strtok_r(s, "-", &sp); // x = "abc", sp = "=-def"
x = strtok_r(NULL, "-=", &sp); // x = "def", sp = NULL
x = strtok_r(NULL, "=", &sp); // x = NULL
// s = "abc\0-def\0"
*/
char *my_strtok_r(char *s, const char *delim, char **save_ptr)
{
char *token;
if (s == NULL)
{
s = *save_ptr;
}
/* Scan leading delimiters. */
s += strspn(s, delim);
if (*s == '\0')
{
return NULL;
}
/* Find the end of the token. */
token = s;
s = strpbrk(token, delim);
if (s == NULL)
{
/* This token finishes the string. */
*save_ptr = strchr(token, '\0');
}
else
{
/* Terminate the token and make *SAVE_PTR point past it. */
*s = '\0';
*save_ptr = s + 1;
}
return token;
}
char *my_strtok(char *s, const char *delim)
{
static char *last;
return my_strtok_r(s, delim, &last);
}
int main(void)
{
char str[80] = "I :love :China!";
const char s[] = ":";
char *token;
/* 获取第一个子字符串 */
token = strtok(str, s);
/* 继续获取其他的子字符串 */
while( token != NULL )
{
printf( "%s\n", token);
token = strtok(NULL, s);
}
strcpy(str, "I :love :China!");
/* 获取第一个子字符串 */
token = my_strtok(str, s);
/* 继续获取其他的子字符串 */
while( token != NULL )
{
printf( "%s\n", token);
token = my_strtok(NULL, s);
}
return 0;
}
18、strupr与strlwr函数
- 原型:
char *strupr(char *str)
char *strlwr(char *str)
- 作用:这两个函数讲一个字符串做大、小写转换
- 单个字符大小写转换见下面函数
#include <ctype.h>
int toupper(int c);
int tolower(int c);
- 源码:
#include <stdio.h>
#include <string.h>
#include <assert.h>
#include <ctype.h>
char *strupr(char *str)
{
assert(str != NULL);
char *s = str;
while (*s != '\0')
{
#if 0
if (*s >= 'a' && *s <= 'z')
{
*s += 'A' - 'a';
}
#endif
*s = toupper(*s);
s++;
}
return str;
}
char *strlwr(char *str)
{
assert(str != NULL);
char *s = str;
while (*s != '\0')
{
#if 0
if (*s >= 'A' && *s <= 'Z')
{
*s += 'a' - 'A';
}
#endif
*s = tolower(*s);
s++;
}
return str;
}
int main()
{
char a[] = "HELLOworld!!!";
printf("%s\n", a);
strlwr(a);
printf("%s\n", a);
strupr(a);
printf("%s\n", a);
return 0;
}
19、memcpy与memccpy函数
- 原型:
void *memcpy(void *dest, const void *src, size_t n)
- 作用:
- 源码:
#include <stdio.h>
#include <assert.h>
void *my_memcpy(void *dest, const void *src, size_t n)
{
assert((dest != NULL) && (src != NULL));
void *o_dest = dest;
char *dest_c = (char *)dest;
const char *src_c = (const char *)src;
while (n--)
{
*dest_c++ = *src_c++;
}
return o_dest;
}
void print_array(int *a, int n)
{
int i;
for (i = 0; i < n; i++)
{
printf(" %d ", a[i]);
}
printf("\n");
}
int main()
{
int a[10] = {1, 2, 3, 4, 5, 66, 777, 8888, 99999, 123456789};
int b[10];
my_memcpy(b, a, sizeof(a));
print_array(a, 10);
print_array(b, 10);
return 0;
}
原型:void *memccpy(void *dest, const void *src, int c, size_t n)
头文件:#include <string.h>
- 功能:由src所指内存区域复制不多于count个字节到dest所指内存区域,如果遇到字符c则停止复制。
- 说明:返回指向字符c后的第一个字符的指针,如果src前n个字节中不存在c则返回NULL。c被复制。
#include <stdio.h>
#include <string.h>
#include <assert.h>
void *my_memccpy(void *dest, const void *src, int c, size_t n)
{
assert((dest != NULL) && (src != NULL));
char *dest_c = (char *)dest;
const char *src_c = (const char *)src;
while (n--)
{
*dest_c = *src_c;
if (*src_c == (char)c)
{
return dest_c + 1;
}
dest_c++;
src_c++;
}
return NULL;
}
int main()
{
const char *src = "Taiyuan BHU";
char dest[20];
char *p;
p = (char*)(my_memccpy(dest, src, 'i', strlen(src)));
if (p != NULL)
{
*p = '\0';
printf("%s\n", dest);
}else
{
printf("char %c is not found\n", 'i');
}
return 0;
}
20、memchr函数
- 原型:
void *memchr(const void *str, int c, size_t n)
- 功能:在参数 str 所指向的字符串的前 n 个字节中搜索第一次出现字符 c(一个无符号字符)的位置。
参数
- str – 指向要执行搜索的内存块。
- c – 以 int 形式传递的值,但是函数在每次字节搜索时是使用该值的无符号字符形式。
- n – 要被分析的字节数。
返回值
该函数返回一个指向匹配字节的指针,如果在给定的内存区域未出现字符,则返回 NULL。
使用:
#include <stdio.h>
#include <string.h>
int main ()
{
const char str[] = "http://www.sohu.com";
const char ch = '.';
char *p;
p = (char*)memchr(str, ch, strlen(str));
printf("|%c| 之后的字符串是 - |%s|\n", ch, p);
return(0);
}
void *memrchr(const void *s, int c, size_t n);
此函数与memchr()函数的功能类似,但是反向搜索
注意:此函数不能在老版gcc下使用,但是可以在g++下使用。
#include <stdio.h>
#include <assert.h>
#include <string.h>
int main ()
{
const char str[] = "http://www.sohu.com";
const char ch = '.';
char *p;
p = (char*)memrchr(str, ch, strlen(str));
printf("|%c| 之后的字符串是 - |%s|\n", ch, p);
return(0);
}
21、memcmp函数
- 原型
int memcmp(const void *str1, const void *str2, size_t n)
- 把存储区 str1 和存储区 str2 的前 n 个字节进行比较。
- 参数
- str1 – 指向内存块的指针。
- str2 – 指向内存块的指针。
- n – 要被比较的字节数。 返回值
- 如果返回值 < 0,则表示 str1 小于 str2。
- 如果返回值 > 0,则表示 str2 小于 str1。
- 如果返回值 = 0,则表示 str1 等于 str2。
- 代码
#include <stdio.h>
#include <assert.h>
#include <string.h>
int my_memcmp(const void *s1, const void *s2, size_t n)
{
assert((s1 != NULL) && (s2 != NULL));
const char *s1_c = s1;
const char *s2_c = s2;
while (n--)
{
if (*s1_c != *s2_c)
{
break;
}
s1_c++;
s2_c++;
}
return *s1_c - *s2_c;
}
int main ()
{
char str1[15];
char str2[15];
int ret;
memcpy(str1, "123456", 6);
memcpy(str2, "123455", 6);
ret = memcmp(str1, str2, 5);
if(ret > 0)
{
printf("str2 小于 str1\n");
}
else if(ret < 0)
{
printf("str1 小于 str2\n");
}
else
{
printf("str1 等于 str2\n");
}
return(0);
}
strcmp与memcmp区别
对于memcmp(),如果两个字符串相同而且count大于字符串长度的话,memcmp不会在\0处停下来,会继续比较\0后面的内存单元,直到不相等或者达到count次数。
对于strncmp()比较必定会在最短的字符串的末尾停下来,即使count还未为零。具体的例子:
char a1[]=“ABCD”;
char a2[]=“ABCD”;
对于memcmp(a1, a2, 10),memcmp在两个字符串的\0之后继续比较
对于strncmp(a1, a2, 10),strncmp在两个字符串的末尾停下,不再继续比较。
所以,如果想使用memcmp比较字符串,要保证count不能超过最短字符串的长度,否则结果有可能是错误的。
strncmp(“abcd”, “abcdef”, 6) = 0。比较次数是一样的:
memcmp:在比较到第5个字符也就是’\0’,*su1 - *su2的结果显然不等于0,所以满足条件跳出循环,不会再进行后面的比较。我想在其他情况下也一样。
strncmp:同样的道理再比较到第5个字符时结束循环,其实strncmp中会判断字符串结束,其作用仅在于当两个字符串相同的情形下,防止多余的比较次数。
22、memmove函数
函数作用:用于从src拷贝count个字节到dest,如果目标区域和源区域有重叠的话,memmove能够保证源串在被覆盖之前将重叠区域区域的字节拷贝到目标区域中,但复制后src内容会被更改,但是当目标区域与源区域没有重叠则和memcpy函数功能相同。
memmove可以被分为三种情况:
1.dest < src,源内存的首地址大于目标内存的首地址,进行正向拷贝,此时无需担心dest + n>src,超出的部分只会将src已经拷贝过的部分覆盖,不会造成影响
2.dest >= src + n,此时目标内存的首地址大于源内存的首地址 +n,进行反向拷贝,由于两首地址相距足够大,无需担心被覆盖问题
3.dest < src + n,此时目标内存的首地址小于源内存的首地址+n,此时随着src向dest拷贝,src前段会将末端的覆盖掉,导致数据丢失,此时我们需要手动“扩容”,将两个指针同时增加n - 1
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
void *my_memmove(void *dest, void *src, size_t n)
{
if (dest == NULL || src == NULL)
{
return NULL;
}
char *dest_c = (char *)dest;
char *src_c = (char *)src;
if (dest_c < src_c || (char *)dest_c >= (char *)src_c + n )
{
while (n--)
{
*dest_c++ = *src_c++;
}
}
else
{
dest_c = dest_c + n - 1;
src_c = src_c + n - 1;
while (n--)
{
*dest_c-- = *src_c--;
}
}
return dest;
}
int main()
{
char src[] = "hello";
char dest[] = "world";
my_memmove(dest, src, strlen(src));
printf("%s\n", dest);
return 0;
}
23、memset函数
原型:void *memset(void *s, int c, size_t n);
作用:复制字符 c(一个无符号字符)到参数 str 所指向的字符串的前 n 个字符。
源码:
#include <stdio.h>
#include <string.h>
#include <assert.h>
void *my_memset(void *s, int c, size_t n)
{
assert(s != NULL);
char *s_c = (char *)s;
while (n--)
{
*s_c++ = (char)c;
}
return s;
}
int main ()
{
char str[50];
strcpy(str,"This is string.h library function");
puts(str);
my_memset(str, 'A', 5);
puts(str);
return(0);
}
另外:
#include <strings.h>
void bzero(void *s, size_t n);
bzero的实现:
memeset(s, 0, n);
次出现字符 c(一个无符号字符)的位置。
参数
- str – 指向要执行搜索的内存块。
- c – 以 int 形式传递的值,但是函数在每次字节搜索时是使用该值的无符号字符形式。
- n – 要被分析的字节数。
返回值
该函数返回一个指向匹配字节的指针,如果在给定的内存区域未出现字符,则返回 NULL。
使用:
#include <stdio.h>
#include <string.h>
int main ()
{
const char str[] = "http://www.sohu.com";
const char ch = '.';
char *p;
p = (char*)memchr(str, ch, strlen(str));
printf("|%c| 之后的字符串是 - |%s|\n", ch, p);
return(0);
}
void *memrchr(const void *s, int c, size_t n);
此函数与memchr()函数的功能类似,但是反向搜索
注意:此函数不能在老版gcc下使用,但是可以在g++下使用。
#include <stdio.h>
#include <assert.h>
#include <string.h>
int main ()
{
const char str[] = "http://www.sohu.com";
const char ch = '.';
char *p;
p = (char*)memrchr(str, ch, strlen(str));
printf("|%c| 之后的字符串是 - |%s|\n", ch, p);
return(0);
}
21、memcmp函数
- 原型
int memcmp(const void *str1, const void *str2, size_t n)
- 把存储区 str1 和存储区 str2 的前 n 个字节进行比较。
- 参数
- str1 – 指向内存块的指针。
- str2 – 指向内存块的指针。
- n – 要被比较的字节数。 返回值
- 如果返回值 < 0,则表示 str1 小于 str2。
- 如果返回值 > 0,则表示 str2 小于 str1。
- 如果返回值 = 0,则表示 str1 等于 str2。
- 代码
#include <stdio.h>
#include <assert.h>
#include <string.h>
int my_memcmp(const void *s1, const void *s2, size_t n)
{
assert((s1 != NULL) && (s2 != NULL));
const char *s1_c = s1;
const char *s2_c = s2;
while (n--)
{
if (*s1_c != *s2_c)
{
break;
}
s1_c++;
s2_c++;
}
return *s1_c - *s2_c;
}
int main ()
{
char str1[15];
char str2[15];
int ret;
memcpy(str1, "123456", 6);
memcpy(str2, "123455", 6);
ret = memcmp(str1, str2, 5);
if(ret > 0)
{
printf("str2 小于 str1\n");
}
else if(ret < 0)
{
printf("str1 小于 str2\n");
}
else
{
printf("str1 等于 str2\n");
}
return(0);
}
strcmp与memcmp区别
对于memcmp(),如果两个字符串相同而且count大于字符串长度的话,memcmp不会在\0处停下来,会继续比较\0后面的内存单元,直到不相等或者达到count次数。
对于strncmp()比较必定会在最短的字符串的末尾停下来,即使count还未为零。具体的例子:
char a1[]=“ABCD”;
char a2[]=“ABCD”;
对于memcmp(a1, a2, 10),memcmp在两个字符串的\0之后继续比较
对于strncmp(a1, a2, 10),strncmp在两个字符串的末尾停下,不再继续比较。
所以,如果想使用memcmp比较字符串,要保证count不能超过最短字符串的长度,否则结果有可能是错误的。
strncmp(“abcd”, “abcdef”, 6) = 0。比较次数是一样的:
memcmp:在比较到第5个字符也就是’\0’,*su1 - *su2的结果显然不等于0,所以满足条件跳出循环,不会再进行后面的比较。我想在其他情况下也一样。
strncmp:同样的道理再比较到第5个字符时结束循环,其实strncmp中会判断字符串结束,其作用仅在于当两个字符串相同的情形下,防止多余的比较次数。
22、memmove函数
函数作用:用于从src拷贝count个字节到dest,如果目标区域和源区域有重叠的话,memmove能够保证源串在被覆盖之前将重叠区域区域的字节拷贝到目标区域中,但复制后src内容会被更改,但是当目标区域与源区域没有重叠则和memcpy函数功能相同。
memmove可以被分为三种情况:
1.dest < src,源内存的首地址大于目标内存的首地址,进行正向拷贝,此时无需担心dest + n>src,超出的部分只会将src已经拷贝过的部分覆盖,不会造成影响
2.dest >= src + n,此时目标内存的首地址大于源内存的首地址 +n,进行反向拷贝,由于两首地址相距足够大,无需担心被覆盖问题
3.dest < src + n,此时目标内存的首地址小于源内存的首地址+n,此时随着src向dest拷贝,src前段会将末端的覆盖掉,导致数据丢失,此时我们需要手动“扩容”,将两个指针同时增加n - 1
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
void *my_memmove(void *dest, void *src, size_t n)
{
if (dest == NULL || src == NULL)
{
return NULL;
}
char *dest_c = (char *)dest;
char *src_c = (char *)src;
if (dest_c < src_c || (char *)dest_c >= (char *)src_c + n )
{
while (n--)
{
*dest_c++ = *src_c++;
}
}
else
{
dest_c = dest_c + n - 1;
src_c = src_c + n - 1;
while (n--)
{
*dest_c-- = *src_c--;
}
}
return dest;
}
int main()
{
char src[] = "hello";
char dest[] = "world";
my_memmove(dest, src, strlen(src));
printf("%s\n", dest);
return 0;
}
23、memset函数
原型:void *memset(void *s, int c, size_t n);
作用:复制字符 c(一个无符号字符)到参数 str 所指向的字符串的前 n 个字符。
源码:
#include <stdio.h>
#include <string.h>
#include <assert.h>
void *my_memset(void *s, int c, size_t n)
{
assert(s != NULL);
char *s_c = (char *)s;
while (n--)
{
*s_c++ = (char)c;
}
return s;
}
int main ()
{
char str[50];
strcpy(str,"This is string.h library function");
puts(str);
my_memset(str, 'A', 5);
puts(str);
return(0);
}
另外:
#include <strings.h>
void bzero(void *s, size_t n);
bzero的实现:
memeset(s, 0, n);