数组

在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]));
  1. &ary[3]: 这会打印数组 ary 第 3 个元素的地址,即 ary[3]

  2. &ary[n]: 这会打印数组 aryn 个元素的地址,即 ary[2]

  3. &ary[n-3]: 这会打印数组 aryn-3 个元素的地址,即 ary[-1]

  4. &ary[n+3]: 这会打印数组 aryn+3 个元素的地址,即 ary[5]

  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] 只是将 nary 位置互换了,但本质上依然是在做指针偏移。

因此,如果 n2,那么 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']

  • 优点:

  • 由于使用了空字符作为结尾,可以处理任意长度的字符串,只要内存足够。

  • 字符串操作函数(如 strlenstrcpystrcmp 等)可以方便地应用于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

charvarcharvarchar2 是用于定义数据库中字符串字段类型的不同数据类型,主要用于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的改进版 varcharvarchar2 是Oracle数据库中的数据类型,类似于 varchar,但在某些方面有更严格的规定。

  • 定义和存储

  • varchar 类似,varchar2(n) 定义的字段最大长度为 n 个字符,实际存储的字符串只占用实际长度的空间。

  • 区别varchar2 明确表示了存储的是字符串数据,Oracle建议在所有新开发中使用 varchar2 而不是 varchar

  • 注意:在未来的Oracle版本中,varchar 可能会被重新定义为与固定长度字符串相关的类型,而 varchar2 则不会发生改变,这也是Oracle建议使用 varchar2 的原因。

  • 使用场景

  • varchar 类似,适合长度可变的文本数据。

区别总结

  • char:固定长度的字符串,存储的字符串长度不足时会用空格填充。

  • varchar:可变长度的字符串,根据实际数据长度存储,只占用实际长度的空间。

  • varchar2:Oracle数据库中特有的可变长度字符串类型,建议优先使用 varchar2 而不是 varchar,以确保未来兼容性。

在选择使用哪种数据类型时,应该根据数据的性质来选择,以平衡性能和空间的使用。