指针
指针是一种特殊的变量,它存储的是另一个变量的内存地址,而不是数据值本身。这个地址指向了内存中的一个特定位置,程序可以通过这个地址来访问或修改存储在该位置的数据。
举个例子:
int var = 10; // 假设var住在内存里的101号房间
int *ptr = &var; // ptr就是我们的笔记本,记下了var的门牌号,也就是101
在这个例子里,var
是住在内存里的一个整数,ptr
是我们的指针笔记本,它记下了var
的地址。我们可以通过ptr
来间接地看看var
房间里的情况:
printf("%d\n", *ptr); // 输出101号房间里的值,也就是10
指针就像是一个超级工具,用得好,可以让你的程序变得又快又灵活。但用得不好,也容易让程序出错。所以,用指针的时候要特别小心。
指针变量
指针变量存储的是另一个变量的内存地址。声明指针变量时,需要指定其指向的数据类型。例如,int *p;
声明了一个指向int
类型数据的指针。
取地址运算符和间接寻址运算符
- 取地址运算符(&):用于获取变量的内存地址。例如,对于变量
int var = 10;
,表达式&var
将得到var
的内存地址。 - 间接寻址运算符(*):用于访问指针指向的内存地址上的数据。如果
p
是一个指针,*p
将提供p
所指向的变量的值。
取地址运算符
取地址运算符&
允许你获取一个变量的内存地址。这个地址可以被赋给一个指针变量,使得该指针指向该变量。
int var = 10;
int *p = &var; // p 现在存储 var 的地址
间接寻址运算符
间接寻址运算符*
允许你访问指针所指向的内存地址上的数据。
int var = 10;
int *p = &var;
printf("%d\n", *p); // 输出 var 的值,即 10
指针赋值
你可以将一个新地址赋给指针变量,或者将NULL
赋给指针以表明它不指向任何地址。
int a = 5, b = 7;
int *p = &a; // p 指向 a
p = &b; // 现在 p 指向 b
指针作为参数
指针可以作为函数参数传递,允许函数通过修改指针来间接修改其他变量的值。
void increment(int *ptr) {
(*ptr)++;
}
int main() {
int num = 5;
increment(&num); // num 现在是 6
}
指针作为返回值
函数可以返回指针类型的值,通常用于返回动态分配的内存地址或数组。
int *createArray(int size) {
int *arr = malloc(size * sizeof(int)); // 分配动态内存
return arr; // 返回新分配数组的指针
}
指针是C语言中一个强大但需要谨慎使用的工具。不当的使用可能会导致内存泄漏、指针悬挂、野指针等问题。理解并正确使用指针对于编写高效且可靠的C程序至关重要。
在C语言中,指针和数组紧密相关,它们共同构成了语言操作内存的基础。以下是对第12章“指针和数组”内容的介绍,包括示例代码:
指针的算术运算
指针的算术运算允许我们通过改变指针的值来遍历数组。
-
指针加上整数:将整数加到指针上,使指针移动到数组的下一个元素。
int array[5] = {1, 2, 3, 4, 5}; int *p = array; p += 1; // p 现在指向 array[1]
-
指针减去整数:从指针减去整数,使指针反向移动。
p -= 1; // p 现在指向 array[0]
-
两个指针相减:计算两个指针之间的距离,它们必须指向同一个数组。
int *q = array + 4; int distance = q - p; // 距离是 4
-
** 指针比较**:比较两个指针是否指向相同的位置。
if (p == q) { // 指针指向相同的元素 }
指针用于数组处理
指针可以用于访问和遍历数组元素。
for (int *ptr = array; ptr < array + 5; ptr++) {
printf("%d ", *ptr);
}
用数组名作为指针
数组名可以作为指向数组第一个元素的指针。
-
数组型实际参数:在函数中,数组作为参数传递时退化为指针。
void printArray(int arr[], int size) { for (int i = 0; i < size; i++) { printf("%d ", arr[i]); } }
-
用指针作为数组名:通过指针运算访问数组元素。
int *ptr = array; printf("%d\n", ptr[1]); // 等同于 array[1]
指针和多维数组
多维数组在内存中是连续存储的,可以通过指针进行操作。
-
处理多维数组的元素:直接通过指针访问多维数组的特定元素。
int multi[2][3] = {{1, 2, 3}, {4, 5, 6}}; int *ptr = &multi[0][0]; printf("%d\n", *(ptr + 5)); // 输出 6
-
处理多维数组的行:将指针移动到下一行的开始。
ptr += 3; // 现在 ptr 指向第二行的开始
-
处理多维数组的列:在行内移动指针来遍历列。
for (int i = 0; i < 3; i++) { printf("%d ", *(ptr + i)); }
-
用多维数组名作为指针:多维数组名退化为指向数组首元素的指针。
printf("%d\n", *multi); // 输出 1,即第一个元素
C99中的指针和变长数组
C99标准引入了变长数组(VLA),允许数组的大小在运行时确定。
int size = 10;
int vla[size][size];
int *p = vla[0]; // p 是指向变长数组第一行的指针
变长数组提供了更多的灵活性,但它们的生命周期仅限于定义它们的块(例如函数)。
指针和数组的结合使用为C语言提供了强大的内存操作能力,但也需要程序员具备对内存布局和指针运算的深刻理解。
字符串
字符串处理是一项基础但非常重要的技能。
字符串字面量
什么是字符串字面量?字符串字面量就是那些在代码中直接写出来的字符串,比如"Hello, World!"
。
-
转义序列:在字符串里,有些特殊字符需要用转义序列表示,比如
\n
表示换行,\t
表示制表符。#include <stdio.h> int main() { // 演示转义序列 char *text = "C语言中的转义序列包括:\\n 表示换行,\\t 表示制表符,\\' 表示单引号,\\\" 表示双引号,\\\\ 表示反斜线。"; printf("%s\n", text); return 0; }
在这个例子中,
\\n
会被替换为换行符,导致文本在输出时开始新的一行;\\t
会被替换为制表符,通常用来增加文本的缩进或对齐;\\'
和\\"
分别用来表示单引号和双引号,因为这两个字符在字符串字面量中有特殊用途;\\\\
用来表示反斜线本身。当你运行这段代码,输出将是:
C语言中的转义序列包括: 表示换行, 表示制表符,' 表示单引号," 表示双引号,\ 表示反斜线。
注意,每个转义序列都以反斜线
\
开始,这是告诉编译器,接下来的字符不是它字面上的意思,而是有特殊的含义。
延续字符串字面量
如果你的字符串太长,一行放不下,你可以用反斜线\
在行尾延续到下一行,这样做不会影响字符串的内容,编译器会将这两行合并为一行来处理。
#include <stdio.h>
int main() {
// 这个字符串太长了,我们用反斜线延续到下一行
const char *longString = "This is a very long string that we need to split"
" across multiple lines to keep our code tidy and"
" readable. This technique is useful for long"
" URLs, file paths, or multi-line messages.";
printf("%s\n", longString);
return 0;
}
如何存储字符串字面量
字符串字面量在C语言中通常存储在程序的只读数据段(通常是内存中的一个特殊区域)中。这意味着它们是不可修改的,任何尝试修改字符串字面量的代码都可能导致编译错误或程序崩溃。
字符串字面量在内存中的存储格式是字符数组,以空字符('\0'
)结尾,这个空字符是一个隐含的终止符,用来标识字符串的结束。C语言标准库中的字符串处理函数通常使用这个终止符来确定字符串的长度。
#include <stdio.h>
int main() {
// 字符串字面量
const char *greeting = "Hello, World!";
// 打印字符串字面量
printf("%s\n", greeting);
// 尝试修改字符串字面量 - 这将导致编译错误
// greeting[0] = 'h'; // 错误:不允许修改指向常量字符串的指针
return 0;
}
在这个例子中,greeting
是一个指向字符串字面量 "Hello, World!"
的指针。这个字符串字面量存储在只读数据段中,greeting
指针被声明为 const
类型,意味着它指向的内容是不可修改的。
当你运行这段代码时,printf
函数会输出字符串字面量:
Hello, World!
尝试修改字符串字面量(如注释中的代码)会导致编译错误,因为 const
限定了指针指向的数据是只读的。
字符串字面量的操作
字符串字面量的这种存储方式使得它们在程序中非常安全,可以防止意外的修改,同时也使得字符串字面量在程序中的使用非常高效,因为它们可以在多个地方被重复使用而不需要额外的内存开销。
字符串字面量虽然存储在只读内存中,不能被直接修改,但你可以对它们执行多种操作。这些操作包括读取、连接、比较、搜索、复制等。以下是一些常见的字符串字面量操作的例子:
示例代码:
#include <stdio.h>
#include <string.h> // 引入字符串处理函数的头文件
int main() {
// 定义两个字符串字面量
const char *greeting = "Hello";
const char *farewell = "Goodbye";
// 打印字符串字面量
printf("Greeting: %s\n", greeting);
printf("Farewell: %s\n", farewell);
// 连接字符串字面量 - 注意结果需要存储在字符数组中
char message[30];
strcpy(message, greeting); // 首先复制 greeting 到 message
strcat(message, " "); // 然后在 message 后添加一个空格
strcat(message, farewell); // 最后添加 farewell
printf("Message: %s\n", message);
// 比较字符串字面量
if (strcmp(greeting, "Hello") == 0) {
printf("The greeting is correct.\n");
}
// 搜索字符串中的子串
const char *sub = "lo";
if (strstr(greeting, sub) != NULL) {
printf("Sub-string '%s' found in greeting.\n", sub);
}
// 获取字符串长度
printf("Length of the greeting: %zu\n", strlen(greeting));
return 0;
}
在这个例子中,我们定义了两个字符串字面量 greeting
和 farewell
。然后执行了以下操作:
- 打印字符串:使用
printf
函数输出字符串字面量的内容。 - 连接字符串:使用
strcpy
将greeting
复制到字符数组message
中,然后使用strcat
将farewell
连接到message
的末尾。注意,连接操作不能直接在字符串字面量上执行,因为它们是常量。 - 比较字符串:使用
strcmp
函数比较greeting
是否等于文本"Hello"
。 - 搜索子串:使用
strstr
函数在greeting
中搜索子串"lo"
。 - 获取长度:使用
strlen
函数获取greeting
的长度。
这些操作展示了如何使用C语言标准库中的字符串处理函数来操作字符串字面量。由于字符串字面量是不可变的,任何修改结果的操作都需要将结果存储在可变的字符数组或字符指针中。
字符串字面量与字符常量
字符串字面量和字符常量在C语言中都是用来表示文本的,但它们之间有几个明显的区别:
-
表示形式:
- 字符串字面量:使用双引号
" "
包围,可以包含多个字符,如"Hello"
。 - 字符常量:使用单引号
' '
包围,只能包含一个字符,如'A'
。
- 字符串字面量:使用双引号
-
内存表示:
- 字符串字面量:在内存中通常以字符数组的形式存在,且以空字符
'\0'
结尾,表示字符串的结束。 - 字符常量:在内存中通常只占用一个字节,表示单个字符的ASCII码值。
- 字符串字面量:在内存中通常以字符数组的形式存在,且以空字符
-
用途:
- 字符串字面量:用于表示一段文本,如消息、文件名等。
- 字符常量:用于表示单个字符,如变量的初始值或特定的控制字符。
-
存储位置:
- 字符串字面量:通常存储在程序的只读数据段(如文本段)。
- 字符常量:可以存储在任何类型的内存区域,包括栈、堆或全局数据段。
-
可修改性:
- 字符串字面量:由于存储在只读区域,不能被修改。
- 字符常量:虽然单个字符是常量,但如果字符存储在可写的内存区域,其值可以被修改。
-
示例代码:
// 字符串字面量示例 const char *greeting = "Hello, World!"; // 不能修改 // 字符常量示例 char letter = 'A'; // 可以修改,例如 letter = 'B';
理解这些区别有助于在编程时正确地使用字符串字面量和字符常量,以表达预期的值和行为。
字符串变量
字符串变量在C语言中通常指的是用来存储字符串的内存位置。具体来说,字符串变量可以是以下几种形式:
-
字符数组:这是最常见的字符串变量形式,用方括号
[]
定义,可以指定大小或让编译器根据初始化的字符串字面量自动计算大小。char myString[10] = "Hello"; // 固定大小的字符数组
-
字符指针:指针变量,可以指向字符串字面量或字符数组的首地址。
char *myStringPointer = "Hello"; // 指向字符串字面量的指针
-
动态分配的字符串:使用
malloc
或calloc
等函数在堆上动态分配的内存,用于存储字符串。char *dynamicString = malloc(10 * sizeof(char)); // 动态分配的内存 if (dynamicString != NULL) { strcpy(dynamicString, "Hello"); }
-
字符数组的指针:这通常是指指向字符数组的指针变量,它可以用于数组的遍历或操作。
char anotherString[] = "World"; char *stringIterator = anotherString; // 指向字符数组的指针
字符串变量在使用时需要考虑几个关键点:
- 初始化:字符串变量可以被初始化为字符串字面量或为空(
NULL
)。 - 空字符:字符串变量应该以空字符
'\0'
结尾,这是字符串结束的标准表示。 - 内存管理:如果使用动态内存分配,需要手动管理内存,使用
free
函数释放不再使用的内存。 - 可变性:字符串变量(特别是字符数组)的内容是可以被修改的,与存储在只读内存区域的字符串字面量不同。
初始化字符串变量
变量在使用前都要先进行初始化,那么字符串变量怎么进行初始化呢?
字符数组初始化
-
使用字符串字面量初始化: 当你声明一个字符数组并用字符串字面量初始化时,数组会自动以空字符(
'\0'
)结尾。char myString[10] = "Hello"; // 包含5个字符和一个隐含的空字符
-
使用空字符显式初始化: 你也可以只使用一个空字符来初始化字符数组,表示它是一个空字符串。
char emptyString[1] = { '\0' }; // 空字符串
字符指针初始化
-
指向字符串字面量: 字符指针可以在声明时指向一个字符串字面量。
char *stringPointer = "Hello"; // stringPointer指向字符串字面量
-
初始化为NULL: 指针可以被初始化为
NULL
,表示它不指向任何地址。char *anotherPointer = NULL; // anotherPointer不指向任何地方
动态分配的字符串初始化
-
分配内存后初始化: 使用
malloc
或calloc
分配内存后,可以手动复制字符串字面量的内容到分配的内存中。char *dynamicString = malloc(10 * sizeof(char)); // 分配内存 if (dynamicString != NULL) { strcpy(dynamicString, "Hello"); // 将字符串字面量复制到分配的内存中 }
-
使用
calloc
初始化:calloc
函数会分配内存并自动将所有字节初始化为0,对于字符串来说,这意味着它会自动以空字符结尾。复制char *callocString = calloc(10, sizeof(char)); // 分配并初始化为0 if (callocString != NULL) { strcpy(callocString, "Hello"); // 复制字符串 }
字符串的读和写
使用printf
函数和puts
函数写字符串
printf
函数可以用来格式化输出,包括字符串。puts
函数用于输出字符串并自动在末尾添加换行符。
示例:
#include <stdio.h>
int main() {
const char *greeting = "Hello, World!";
// 使用printf输出字符串
printf("%s\n", greeting);
// 使用puts输出字符串
puts(greeting);
return 0;
}
使用scanf
函数和gets
函数读字符串
scanf
函数可以用来从标准输入读取格式化数据,包括字符串。gets
函数已不推荐使用,因为它可能导致缓冲区溢出,应该使用fgets
代替。
示例 (使用scanf
):
#include <stdio.h>
int main() {
char name[50];
// 使用scanf读取字符串
printf("What's your name? ");
scanf("%s", name); // 注意:%s会停在空白字符前
// 输出读取的字符串
printf("Nice to meet you, %s!\n", name);
return 0;
}
逐个字符读字符串
逐个字符读取字符串通常使用getchar
函数,它可以一次读取一个字符。
示例:
#include <stdio.h>
int main() {
char ch;
printf("Enter characters: ");
while ((ch = getchar()) != '\n') { // 读取字符直到遇到换行符
putchar(ch); // 将读取的字符输出
}
return 0;
}
注意事项
- 使用
scanf
读取字符串时,如果输入的字符串超过数组的大小,可能会导致缓冲区溢出。使用fgets
可以避免这个问题。 gets
函数是不安全的,因为它不检查缓冲区大小,已经被废弃。使用fgets
函数代替,它允许指定接收字符的最大数量。
使用fgets
示例:
复制#include <stdio.h>
int main() {
char name[50];
// 使用fgets安全读取一行输入
if (fgets(name, sizeof(name), stdin) != NULL) {
// 移除fgets读取的末尾换行符
name[strcspn(name, "\n")] = 0;
printf("Nice to meet you, %s!\n", name);
}
return 0;
}
访问字符串中的字符
1. 使用字符数组
当你有一个字符数组时,你可以直接通过索引来访问或修改字符串中的字符。
示例:
#include <stdio.h>
int main() {
char charArray[] = "Hello, World!";
// 访问并打印第一个字符
printf("The first character is: %c\n", charArray[0]);
// 修改第二个字符
charArray[1] = 'a';
// 打印修改后的字符串
printf("Modified string: %s\n", charArray);
return 0;
}
2. 使用字符指针
如果你有一个指向字符串的字符指针,你可以通过增加指针的值来访问字符串中的每个字符。
示例:
#include <stdio.h>
int main() {
const char *charPointer = "Hello, World!";
// 使用循环访问字符串中的每个字符
for (int i = 0; charPointer[i] != '\0'; ++i) {
printf("Character at position %d is: %c\n", i, charPointer[i]);
}
return 0;
}
3. 使用strlen
函数
strlen
函数可以用来获取字符串的长度,然后你可以使用这个长度来访问字符串中的字符。
示例:
#include <stdio.h>
#include <string.h>
int main() {
const char *str = "Hello, World!";
size_t length = strlen(str);
// 访问最后一个字符
if (length > 0) {
printf("The last character is: %c\n", str[length - 1]);
}
return 0;
}
4. 使用strchr
函数
strchr
函数可以用来查找字符串中某个字符的第一次出现的位置。
示例:
#include <stdio.h>
#include <string.h>
int main() {
const char *str = "Hello, World!";
char toFind = 'o';
char *found = strchr(str, toFind);
if (found != NULL) {
printf("Found character '%c' at position: %ld\n", toFind, found - str);
}
return 0;
}
注意事项
- 字符串在内存中以字符数组的形式存在,并且以空字符
'\0'
结尾。 - 访问字符串时,要确保不要超出字符串的实际长度,否则可能导致未定义行为。
- 使用字符指针访问字符串时,通常需要检查空字符来确定字符串的结束位置。
- 函数如
strchr
等可以在字符串中搜索特定的字符,但它们返回的是指针,指向找到的第一个匹配项。
通过这些方法,你可以方便地访问和操作C语言中字符串的字符。
使用C语言的字符串库
C语言的字符串库提供了一组强大的函数,用于执行各种字符串操作。以下是它们的基本用法和示例:
strcpy
函数(字符串复制)
strcpy
函数用于将一个字符串复制到另一个字符串中。它从源字符串复制所有字符,包括空字符'\0'
。
示例:
#include <stdio.h>
#include <string.h>
int main() {
char src[] = "Source String";
char dest[15];
strcpy(dest, src); // 将src复制到dest
printf("Copied string: %s\n", dest);
return 0;
}
strlen
函数(字符串长度)
strlen
函数返回字符串的长度,不包括结尾的空字符'\0'
。
示例:
#include <stdio.h>
#include <string.h>
int main() {
const char *str = "Hello, World!";
size_t length = strlen(str);
printf("Length of the string: %zu\n", length);
return 0;
}
strcat
函数(字符串连接)
strcat
函数用于将一个字符串追加到另一个字符串的末尾。
注意:使用strcat
之前,需要确保目标字符串有足够的空间来容纳源字符串。
示例:
#include <stdio.h>
#include <string.h>
int main() {
char str1[20] = "Hello, ";
const char *str2 = "World!";
strcat(str1, str2); // 将str2追加到str1的末尾
printf("Concatenated string: %s\n", str1);
return 0;
}
strcmp
函数(字符串比较)
strcmp
函数用于比较两个字符串。如果两个字符串相等,它返回0
;如果第一个字符串在字典序上小于第二个字符串,它返回一个负数;如果大于,返回一个正数。
示例:
#include <stdio.h>
#include <string.h>
int main() {
const char *str1 = "Apple";
const char *str2 = "apple";
int result = strcmp(str1, str2);
if (result == 0) {
printf("The strings are identical.\n");
} else if (result < 0) {
printf("The first string is less than the second.\n");
} else {
printf("The first string is greater than the second.\n");
}
return 0;
}
注意事项
- 使用
strcpy
和strcat
时,要特别注意目标字符串的缓冲区大小,避免缓冲区溢出。 strlen
返回的长度是字符串中字符的数量,不包括结尾的空字符。strcmp
在比较时是区分大小写的。如果需要不区分大小写的比较,可以使用strcasecmp
(在某些系统上)。
字符串惯用法
1. 搜索字符串结尾
通常使用NULL
字符('\0'
)来标识字符串的结尾。遍历字符串时,检查每个字符是否为NULL
。
示例:
#include <stdio.h>
int main() {
const char *str = "Hello, World!";
while (*str) { // 继续循环直到遇到'\0'
printf("%c", *str++);
}
return 0;
}
2. 复制字符串
使用strcpy
函数复制字符串时,要确保目标数组足够大,以避免溢出。
示例(安全的字符串复制):
#include <stdio.h>
#include <string.h>
int main() {
char src[] = "Source String";
char dest[15] = {0}; // 初始化为0,确保有空间
// 确保源字符串不会溢出目标数组
if (strlen(src) < sizeof(dest)) {
strcpy(dest, src);
printf("Copied string: %s\n", dest);
} else {
printf("Source string is too long.\n");
}
return 0;
}
3. 连接字符串
使用strcat
函数连接字符串前,检查目标字符串是否有足够的空间。
示例:
#include <stdio.h>
#include <string.h>
int main() {
char str1[20] = "Hello, ";
const char *str2 = "World!";
// 确保连接后不会溢出
if (strlen(str1) + strlen(str2) + 1 <= sizeof(str1)) {
strcat(str1, str2);
printf("Concatenated string: %s\n", str1);
} else {
printf("Not enough space to concatenate.\n");
}
return 0;
}
4. 比较字符串
使用strcmp
函数比较两个字符串,考虑使用strcasecmp
进行不区分大小写的比较。
示例:
#include <stdio.h>
#include <string.h>
int main() {
const char *str1 = "Apple";
const char *str2 = "apple";
if (strcasecmp(str1, str2) == 0) {
printf("The strings are identical (case-insensitive).\n");
}
return 0;
}
5. 动态字符串操作
使用动态内存分配时,要记得释放不再使用的内存。
示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
char *dynamicString = malloc(10 * sizeof(char));
if (dynamicString != NULL) {
strcpy(dynamicString, "Hello");
// 处理字符串...
free(dynamicString); // 释放内存
}
return 0;
}
6. 使用strncpy
和strncat
进行安全的复制和连接
这些函数限制了复制或连接的字符数,有助于防止溢出。
示例:
#include <stdio.h>
#include <string.h>
int main() {
char src[] = "Longer than 15 characters";
char dest[15];
strncpy(dest, src, sizeof(dest) - 1); // 复制前14个字符
dest[sizeof(dest) - 1] = '\0'; // 确保字符串以'\0'结尾
printf("Truncated string: %s\n", dest);
return 0;
}
7. 避免使用gets
gets
函数是不安全的,因为它不检查缓冲区大小,应该避免使用,改用fgets
。
示例:
复制#include <stdio.h>
int main() {
char buffer[100];
printf("Enter a line of text: ");
fgets(buffer, sizeof(buffer), stdin); // 安全读取一行文本
// 处理字符串...
return 0;
}