08、9_C基础(数组与字符串)
数组
在C语言中,数组中的元素默认情况下是对齐的。对齐是指编译器在内存中存储数据时,会将数据存储在符合其数据类型大小的内存地址边界上,以提高访问速度和性能。
例如,对于一个整数数组,由于每个整数通常占用4个字节(在32位或64位系统上),编译器会尝试将每个整数存储在可以被4整除的地址上。如果数组中的元素不对齐,处理器可能需要进行额外的内存访问操作,从而影响性能。
不过,具体的对齐方式可能会因编译器、编译选项、以及数据类型的不同而有所变化。在某些特殊情况下,例如使用#pragma pack
指令或在结构体中使用不同大小的数据类型,可能会导致数据未按默认对齐方式存储。
ary[n] = 5; 实际上就是访问到第n个,然后写入5。
数组名表示的是第0个元素的地址常量;
变量 n
被设为 2,ary
是一个数组。代码尝试访问数组元素的地址,并使用 printf
打印出来。以下是代码中每行的解释:
printf("%p\r\n", &ary[3]);
printf("%p\r\n", &ary[n]);
printf("%p\r\n", &ary[n-3]);
printf("%p\r\n", &ary[n+3]);
printf("%p\r\n", &(n[ary]));
&ary[3]
: 这会打印数组ary
第 3 个元素的地址,即ary[3]
。&ary[n]
: 这会打印数组ary
第n
个元素的地址,即ary[2]
。&ary[n-3]
: 这会打印数组ary
第n-3
个元素的地址,即ary[-1]
。&ary[n+3]
: 这会打印数组ary
第n+3
个元素的地址,即ary[5]
。&(n[ary])
: 这实际上是一个合法的表达方式,因为n[ary]
等价于ary[n]
,这是由于指针算术的特性。
为什么运行不会出错?
未越界访问:C语言本身并不检查数组越界访问,因此如果程序访问了数组范围之外的内存(比如
ary[-1]
或ary[5]
),在大多数情况下,程序不会崩溃。这些访问只是读取内存中其他位置的数据,但这确实是未定义行为,可能导致程序不稳定或其他不可预见的后果。n[ary]
合法性:n[ary]
实际上是合法的,因为n[ary]
等价于ary[n]
。在C语言中,数组名可以被看作指向其第一个元素的指针,而n[ary]
的含义是 *(n + ary) ,即从数组的基地址开始偏移n
个元素的位置。
数组特性
在C语言中,表达式 &(n[ary])
是合法的,这主要归因于指针算术的特性。要理解这一点,我们需要从数组和指针的关系以及指针算术的基本原理出发。
数组与指针的关系
在C语言中,数组名在很多情况下可以被看作是指向数组第一个元素的指针。例如:
int ary[10];
ary
实际上是一个指向 ary[0]
的指针,类型为 int*
。因此,ary[i]
表示从 ary
开始偏移 i
个元素的位置,等价于 *(ary + i)
。
指针算术
在C语言中,指针算术是基于指针类型来进行的。假设 ptr
是一个指向类型 T
的指针:
ptr + 1
指的是指针ptr
增加sizeof(T)
个字节后的地址。ptr - 1
指的是指针ptr
减少sizeof(T)
个字节后的地址。
例如,如果 ptr
是一个指向 int
的指针,而 int
的大小为4字节,那么 ptr + 1
实际上增加了4个字节。
n[ary]
是如何工作的?
n[ary]
这种语法看似奇怪,但它在C语言中是合法的。根据C语言标准,a[b]
等价于 *(a + b)
,这意味着 n[ary]
实际上等价于 *(n + ary)
。也就是说,n[ary]
只是将 n
和 ary
位置互换了,但本质上依然是在做指针偏移。
因此,如果 n
是 2
,那么 n[ary]
等价于 *(ary + 2)
,即 ary[2]
。这同样适用于 ary[n]
,它也等价于 *(ary + n)
。
&(n[ary])
的含义
&(n[ary])
是取 n[ary]
的地址。因为 n[ary]
等价于 ary[n]
,它的地址自然就是 &ary[n]
。
总结
在 C 语言中,由于 n[ary]
等价于 ary[n]
,而 ary[n]
代表的是数组 ary
中第 n
个元素的值,因此 &(n[ary])
是一个合法的表达式,它表示取 ary[n]
的地址。
指针算术的实质
指针算术的本质在于通过指针和数据类型的大小来计算内存地址。a + b
中的指针偏移实际上是基于 a
指向的数据类型来决定的。这种特性使得我们可以通过指针和整数的组合来灵活地访问数组中的不同元素。
有一个思想:
二维数组就是一维数组内,每个元素都是一个一维数组
三维数组是一个一维数组内存放了n个二维数组
总结:多维数组是复杂的一维数组
int ary[x][y]
地址 (type)ary + sizeof(type[y] * x) + sizeof(type) * y == (int)ary + sizeof(int) *( y * x + y*)
字符串
1. strlen
原型:
size_t strlen(const char *str);
用途: 计算字符串的长度(不包括结尾的空字符
\0
)。示例:
char str[] = "Hello, World!";
size_t len = strlen(str); // len = 13
2. strcpy
原型:
char *strcpy(char *dest, const char *src);
用途: 将源字符串
src
复制到目标字符串dest
中,覆盖目标字符串的内容,且包括终止符\0
。示例:
char src[] = "Hello";
char dest[10];
strcpy(dest, src); // dest now contains "Hello"
3. strncpy
原型:
char *strncpy(char *dest, const char *src, size_t n);
用途: 类似于
strcpy
,但最多只复制n
个字符。如果src
的长度小于n
,则目标字符串的剩余部分填充\0
。示例:
char src[] = "Hello";
char dest[10];
strncpy(dest, src, 3); // dest now contains "Hel"
4. strcat
原型:
char *strcat(char *dest, const char *src);
用途: 将源字符串
src
追加到目标字符串dest
的末尾。dest
必须有足够的空间来容纳追加后的字符串。示例:
char dest[20] = "Hello";
char src[] = ", World!";
strcat(dest, src); // dest now contains "Hello, World!"
5. strncat
原型:
char *strncat(char *dest, const char *src, size_t n);
用途: 将源字符串
src
的前n
个字符追加到目标字符串dest
的末尾。示例:
char dest[20] = "Hello";
char src[] = "World!";
strncat(dest, src, 3); // dest now contains "HelloWor"
6. strcmp
原型:
int strcmp(const char *str1, const char *str2);
用途: 比较两个字符串的内容。
如果
str1
小于str2
,返回负值。如果
str1
等于str2
,返回 0。如果
str1
大于str2
,返回正值。示例:
char str1[] = "Hello";
char str2[] = "World";
int result = strcmp(str1, str2); // result < 0 since "Hello" < "World"
7. strncmp
原型:
int strncmp(const char *str1, const char *str2, size_t n);
用途: 比较两个字符串的前
n
个字符。示例:
char str1[] = "Hello";
char str2[] = "Helium";
int result = strncmp(str1, str2, 3); // result == 0 because the first 3 characters are the same
8. strchr
原型:
char *strchr(const char *str, int c);
用途: 在字符串
str
中查找字符c
的首次出现,并返回指向该字符的指针。如果未找到,则返回NULL
。示例:
char str[] = "Hello, World!";
char *ptr = strchr(str, 'W'); // ptr points to "World!"
9. strrchr
原型:
char *strrchr(const char *str, int c);
用途: 查找字符
c
在字符串str
中的最后一次出现,并返回指向该字符的指针。示例:
char str[] = "Hello, World!";
char *ptr = strrchr(str, 'o'); // ptr points to the last "o" in "World!"
10. strstr
原型:
char *strstr(const char *haystack, const char *needle);
用途: 在字符串
haystack
中查找子字符串needle
的首次出现,并返回指向该子字符串的指针。如果未找到,则返回NULL
。示例:
char haystack[] = "Hello, World!";
char needle[] = "World";
char *ptr = strstr(haystack, needle); // ptr points to "World!"
11. memset
原型:
void *memset(void *str, int c, size_t n);
用途: 将内存区域
str
的前n
个字节设置为指定的字符c
。示例:
char str[50];
memset(str, 'A', 50); // Fill str with 50 'A' characters
12. memcpy
原型:
void *memcpy(void *dest, const void *src, size_t n);
用途: 从源内存区域
src
复制n
个字节到目标内存区域dest
。示例:
char src[] = "Hello, World!";
char dest[50];
memcpy(dest, src, strlen(src) + 1); // Copies the entire string including the null terminator
13. memmove
原型:
void *memmove(void *dest, const void *src, size_t n);
用途: 类似于
memcpy
,但处理源和目标内存区域重叠的情况时,memmove
会确保数据不会被覆盖。示例:
char str[] = "Hello, World!";
memmove(str + 7, str, 6); // Move "Hello," to the right, safe to use even when source and destination overlap
字符串风格
C字符串风格
表示方式:
C字符串是以字符数组的形式存在,并且以空字符 (
'\0'
) 作为结尾标志。这个空字符并不计入字符串的长度,但它是字符串的终止符,表明字符串在此结束。例如,一个表示“Hello”的C字符串在内存中的表示是:
['H', 'e', 'l', 'l', 'o', '\0']
。优点:
由于使用了空字符作为结尾,可以处理任意长度的字符串,只要内存足够。
字符串操作函数(如
strlen
、strcpy
、strcmp
等)可以方便地应用于C字符串。缺点:
操作字符串时容易出错,尤其是处理长度时,需要特别小心,以防止缓冲区溢出。
C字符串操作效率较低,尤其是涉及字符串长度的操作时,因为每次计算长度都需要遍历字符串直到遇到
'\0'
。示例:
char str[] = "Hello";
printf("Length of str: %zu\n", strlen(str)); // 输出字符串长度
Pascal字符串风格
表示方式:
Pascal字符串通常以长度前缀来表示字符串。也就是说,字符串的第一个字节(或前几个字节,取决于实现)存储字符串的长度,后面紧跟实际的字符数据。
例如,一个表示“Hello”的Pascal字符串在内存中的表示是:
[5, 'H', 'e', 'l', 'l', 'o']
,其中5
表示字符串的长度。优点:
由于长度存储在字符串的前面,获取字符串长度的操作是常数时间的,不需要遍历整个字符串。
在字符串操作时,能更好地防止缓冲区溢出问题,因为操作函数通常会检查并使用长度信息。
缺点:
字符串的最大长度受长度前缀的大小限制。例如,如果长度前缀是一个字节,则字符串的最大长度为255个字符。
字符串需要特殊的处理函数,不能直接使用C标准库中的字符串处理函数。
示例:
var
str: string;
begin
str := 'Hello';
writeln('Length of str: ', Length(str)); // 输出字符串长度
end;
对比总结
C字符串依赖于空字符
'\0'
作为结尾标志,而Pascal字符串则使用一个长度前缀来表示字符串的长度。C字符串在处理长度时较为灵活,但操作时需要注意避免缓冲区溢出;而Pascal字符串操作较为安全,但受长度前缀的限制。
在不同的编程语言和应用场景中,这两种字符串风格各有优势,选择使用哪种风格通常取决于具体需求和语言规范。
char、varchar以及varchar2
char
、varchar
和 varchar2
是用于定义数据库中字符串字段类型的不同数据类型,主要用于SQL语言中。在不同的数据库管理系统(如Oracle、MySQL、SQL Server等)中,它们有着不同的表现形式和用途。以下是对这三种数据类型的解释:
1. char
固定长度的字符串:
char
类型用于存储固定长度的字符串数据。定义和存储:
当定义一个
char(n)
类型的字段时,字段的长度固定为n
个字符。如果存储的字符串长度不足n
,则自动填充空格以达到n
个字符的长度。例如,
char(10)
定义的字段无论存储 "Hello" 还是 "Hi",实际占用的空间都是 10 个字符的长度,"Hello" 将填充为 "Hello "(后面有5个空格)。使用场景:
适合存储定长数据,如固定长度的编码、身份证号码、固定格式的日期等。
性能:由于长度固定,因此在处理速度上可能会比
varchar
更快,但可能会浪费空间。
2. varchar
可变长度的字符串:
varchar
类型用于存储可变长度的字符串数据。定义和存储:
当定义一个
varchar(n)
类型的字段时,字段的最大长度为n
个字符,但实际存储的字符串只占用实际字符长度的空间。例如,
varchar(10)
定义的字段如果存储 "Hello",实际只占用 5 个字符的空间,而不是 10 个。使用场景:
适合存储长度不确定的字符串数据,如用户输入的文本、描述性信息等。
性能:由于长度可变,节省了存储空间,但在检索和处理时,可能需要额外的开销来计算字符串的实际长度。
3. varchar2
(特定于Oracle数据库)
Oracle的改进版
varchar
:varchar2
是Oracle数据库中的数据类型,类似于varchar
,但在某些方面有更严格的规定。定义和存储:
与
varchar
类似,varchar2(n)
定义的字段最大长度为n
个字符,实际存储的字符串只占用实际长度的空间。区别:
varchar2
明确表示了存储的是字符串数据,Oracle建议在所有新开发中使用varchar2
而不是varchar
。注意:在未来的Oracle版本中,
varchar
可能会被重新定义为与固定长度字符串相关的类型,而varchar2
则不会发生改变,这也是Oracle建议使用varchar2
的原因。使用场景:
与
varchar
类似,适合长度可变的文本数据。
区别总结
char
:固定长度的字符串,存储的字符串长度不足时会用空格填充。varchar
:可变长度的字符串,根据实际数据长度存储,只占用实际长度的空间。varchar2
:Oracle数据库中特有的可变长度字符串类型,建议优先使用varchar2
而不是varchar
,以确保未来兼容性。
在选择使用哪种数据类型时,应该根据数据的性质来选择,以平衡性能和空间的使用。