下面这些是个人的总结。注意这些内容对GNU C 使用,对标准C不一定适用。如果有不对的地方,欢迎大家指正。
-
可以单独定义一个空的指针,但不能单独定义一个空的数组。
int *a; //合法,&a是a的地址,有意义,a是这个指针指向的地址,未定。如果打印出来可能是0,也可能是其他地址。
int a[]; //非法。对C和C++都一样。
但是定义一个维度为0的数组是可以的。
int a[0]; //合法,而且a 和 &a都表示数组a的地址。 -
在struct或class里面可以光定义一个空指针,不可以光定义空数组。
typedef struct
{
int *a;
} A;
//sizeof(A)=8,也就是一个指针的长度。
typedef struct
{
int b[]; //error: flexible array member in a struct with no named members
} B;
B是非法的。
但是如果指定b的维度为0,则是可以的。
typedef struct
{
int b[0];
} B;
sizeof(B)=0。
- 如果在上面含有b[]的B里面增加一个其他字段(不能是空数组),那么B就是合法的。
typedef struct
{
int len;
int b[];
} B;
sizeof(B)=4,也就是int len的长度。这里b[]不占空间,相当于留下一个痕迹。
但要注意,上面这种情况下int b[]只能是结构体中最后一个字段,后面不能再加别的字段。
- 参考下面的例子
https://zhuanlan.zhihu.com/p/378032352
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
char a[0];
printf("%p\n", a);
char b[0];
printf("%p\n", b);
const char *c= "Hello";
printf("%p\n", c);
const char *d = "Hello";
printf("%p\n", d);
return EXIT_SUCCESS;
}
注意编译器有时候会把a和b的地址分配成同一个,因为这两个都是空数组。
但不管怎么样,c和d的地址肯定是同一个,因为"Hello"是字符串常量,放在内存中的字符串常量区,不在栈中。
输出结果为:
0x7fff17fd0216
0x7fff17fd0217
0x55d23d0fb008
0x55d23d0fb008
- 通常编译器会把int a[x]当成*(a + x)。这样a[3]和3[a]其实就是一回事。但是因为上面的空数组非法,我们不能认为当x为空时,int a[]等价于*(a)=*a。
但其他情况大部分应该都是等价的。
比如下面三个函数,其实是一回事。
#include <stdio.h>
int func1(int a[], int len) {
printf("\n%d\n", a[len]);
return 0;
}
int func2(int a[0], int len) { //我发现这里写a[1],a[2],a[3]也是一样的,但写成a[4]以后会有警告“warning: ‘func2’ accessing 16 bytes in a region of size 12 [-Wstringop-overflow=]”,但结果还是对的。
//注意:在算表达式的时候a[2]和2[a]是一回事,但我们不能把int 2[a]或int 0[a]当函数入口参数传进去,编译会出错。
printf("\n%d\n", a[len]);
return 0;
}
int func3(int *a, int len) {
printf("\n%d\n", a[len]);
return 0;
}
int main(int argc, char* argv[]){
int c[3] = {1,2,8};
func1(c, 2);
func2(c, 2);
func3(c, 2);
return 0;
}
- 当我们定义int a[3]时,a和&a都表示a的地址,是等价的。但是,当我们定义int *a时,a表示指针a的值,也就是指针a指向什么地址,而&a表示指针a本身的地址。
那么有意思的事情来了,一个指针p能不能指向它自己的地址呢?如果可以,那么&p, p, *p是什么关系呢?
#include <stdio.h>
int main(int argc, char* argv[]){
int *p = &p;
printf("\n%X, %X, %X\n", p, &p, *p);
return 0;
}
答案当然是可以的。&p, p, *p的值就一样了。不过代表的意思还是不一样的。&p是指针p的地址。p是指针p所指向的地址,*p是指针p所指向的地址所储存的值。
main.c:12:20: warning: format ‘%X’ expects argument of type ‘unsigned int’, but argument 3 has type ‘int **’ [-Wformat=]
12 | printf(“\n%X, %X, %X\n”, p, &p, *p);
| ~^ ~~
| | |
| unsigned int int **
输出结果:
63B1BE80, 63B1BE80, 63B1BE80
- C的编译器不会管数组是否越界,可能严格的编译等级会给警告。它只会根据数组的起始地址往后算出数组元素的地址。
#include <stdio.h>
int main(int argc, char* argv[]){
int e = 6, g = 4, h = 2;
int a[0];
int b = 3, c = 7, d = 9;
printf("%p %p %p %p\n", a, &a[0], &a[1], &a[2]);
printf("%X %X %X\n", a[0], a[1], a[2]);
return 0;
}
运行结果为:
0x7ffdb9a67514 0x7ffdb9a67514 0x7ffdb9a67518 0x7ffdb9a6751c
0 57D07B00 23611D58
我们可以看出a数组并没有分配空间,但是a[0], a[1]和a[2]仍然可以访问。它们的地址就是a的地址往后排。a在栈上它们的地址就在栈上。a在堆上它们的地址就在堆上。
当然这是很危险的,因为很容易就访问到非法地址或其他data的地址了。
不过这个方法为后面的可变长数组埋下了伏笔。
- 指针变量其实也是变量,它也有地址,地址上存的就是它的值。不同于其他变量是因为我们定义它的时候加了一个*号,告诉编译器它存的值表示一个地址而已。
我们知道C函数传参的时候,传值和传指针的区别。传值的时候,函数内部会生成一个值的拷贝。
那传指针的时候,函数内部会不会生成一个指针的拷贝呢?答案是的。不过这个传进来的指针和指针的拷贝的值一样,都表示的同样的地址,这样我们在该地址上修改,结果就会保存下来。而值传递的时候,传进来的参数和它的拷贝的值一样,但是地址不一样。这样我们修改了拷贝,函数返回值之后拷贝就作废了。
#include <stdio.h>
int func(int *a)
{
printf("\nin func(), a=%p, &a=%p, *a=%X", a, &a, *a);
*a = 6;
return 0;
}
int main(int argc, char* argv[])
{
int b = 3;
int *a = &b;
printf("\nbefore func(), a=%p, &a=%p, *a=%X", a, &a, *a);
func(a);
printf("\nafter func(), a=%p, &a=%p, *a=%X", a, &a, *a);
return 0;
}
运行结果:
before func(), a=0x7ffff2496bec, &a=0x7ffff2496bf0, *a=3
in func(), a=0x7ffff2496bec, &a=0x7ffff2496bb8, *a=3
after func(), a=0x7ffff2496bec, &a=0x7ffff2496bf0, *a=6
可见在func()中,&a的值变了,说明func()里面的a也是一个传进来的a的一个拷贝,虽然两个都是指针。
总结一个:其实传值和传指针也没什么本质的区别,函数里面都会给传进来的东西生成一个拷贝,函数返回这个拷贝也就作废了。不同的是:
传指针的话,拷贝也是指针,它们的值都是地址,这个地址在函数调用前就存在,函数调用后也还是存在,这样通过指针的拷贝在地址上修改,函数返回后还是有效的。
传值的话,修改是在拷贝上进行的,函数返回后就没用了。
- 那么我们能不能只用传值来达到传指针的作用呢?当然可以,我们把传的值设为一个地址就够了。下面就是把b的值变成了6,函数返回后还是有效。之所以a用long long 类型是因为64位机器的地址是8个字节,用int的话4个字节没法保存。
#include <stdio.h>
int func(long long a)
{
*(int *)a = 6;
return 0;
}
int main()
{
int b = 3;
long long a = (long long)&b;
func(a);
printf("b=%d", b);
return 0;
}
- 可变长数组 Variable-length-array,简称VLA。下面这个链接参考的
https://blog.csdn.net/zwl1584671413/article/details/122459038
最近在写C代码,经常看到Linux 的头文件中有的结构体后面会定义一个空数组,不知道其为何作用?经过高人指点终于明白其要点!
struct inotify_event {
__s32 wd;
__u32 mask;
__u32 cookie;
__u32 len;
char name[0];
};
如上,结构体最后一个元素name为空数组。
这是个广泛使用的常见技巧,常用来构成缓冲区。如果你是做嵌入式开发,这种技巧应该用得漫天飞了。 比起指针用空数组有这样的优势:
-
不需要初始化,数组名直接就是缓冲区数据的起始地址(如果存在数据)
-
不占任何空间,指针需要占用4 byte长度空间,空数组不占任何空间,节约了空间
“这个数组不占用任何内存”,意味着在计算结构体的size时,不会计算最后一个元素,例如上面sizeof(struct inotify_event) = 16 bytes (前四个元素的内存长度)
这种空数组定义最适合制作动态buffer,因为可以这样分配空间:
malloc( sizeof(struct XXX)+ buff_len );
这样的好处是:直接就把buffer的结构体和缓冲区一块分配了,空数组其实变成了buff_len长度的数组了,一次分配省了不少麻烦。
-
大家知道为了防止内存泄漏,如果是分两次分配(结构体和缓冲区),那么要是第二次malloc失败了,必须回滚释放第一个分配的结构体。这样带来了编码麻烦。
-
其次,分配了第二个缓冲区以后,如果结构里面用的是指针,还要为这个指针赋值。同样,在free这个buffer的时候,用指针也要两次free。如果用空数组,所有问题一次解决
如此看来,用空数组既简化编码,又解决了内存管理问题提高了性能,何乐不为?应该广泛采用。
- 下面这个可变长数组VLA的例子来自
https://zhuanlan.zhihu.com/p/265986375
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
struct student
{
int id;
char sex;
char a[0];
};
int main(void)
{
struct student *s = NULL;
s = malloc(sizeof(struct student) + 20);
if (NULL == s)
{
printf("malloc failed..\n");
return 1;
}
memset(s, 0, sizeof(struct student) + 20);
s->id = 1;
s->sex = 'M';
strcpy(s->a, "hello world");
printf("id: %d sex: %c a: %s\n", s->id, s->sex, s->a);
free(s);
return 0;
}
也可以不用malloc,而是直接在栈上分配student s。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
typedef struct
{
int id;
//char sex;
char a[0];
} student;
int main(void)
{
char str[] = "hello world";
student s;
s.id = 1;
// s.sex = 'M';
//strcpy(s.a, "hello world");
strcpy((char*)(&s + 1), str);
printf("id: %d a: %s\n", s.id, s.a);
return 0;
}
运行结果为:
id: 1 a: hello world
注意,如果把上面的 strcpy((char*)(&s + 1), str); 改成strcpy(s.a, “hello world”); 会有下面的编译警告,因为编译器记得a是没有分配空间的,它会觉得有问题。但结果还是对的。直接用(char *)(&s+1)就没问题了。注意这里&s是student类型的地址,所以它长度为4, +1后就直接指向a数组。
main.c:34:5: warning: ‘__builtin_memcpy’ writing 12 bytes into a region of size 0 overflows the destination [-Wstringop-overflow=]
34 | strcpy(s.a, “hello world”);
| ^~~~~~~~~~~~~~~~~~~~~~~~~~
id: 1 a: hello world
注意我把上面的char sex字段给删除了,下面是不删除的代码
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
typedef struct
{
int id;
char sex;
char a[0];
} student;
int main(void)
{
char str[] = "hello world";
student s;
s.id = 1;
s.sex = 'M';
strcpy((char*)(&s + 1), str);
printf("id: %d sex: %c, a: %s\n", s.id, s.sex, s.a);
return 0;
}
该代码会输出
id: 1 sex: M, a:
为什么a数组没内容了?我觉得这是由于char sex字段后面有3个字节的padding引起的。在int a[]字段前面应该不要有padding, 否则VLA就不好用了。
- 下面这个VLA的例子来自https://www.jianshu.com/p/cdc4c3ffc031,不过我加了很多修改的版本比较。
#include <stdio.h>
#include <malloc.h>
#include <string.h>
typedef struct line
{
int len;
char contents[0]; // 修改的地方。
}line;
int main(int argc, char **argv)
{
char str[] = "hello world";
struct line *ptr = (struct line*)malloc(sizeof(line) + strlen(str) + 1);
// struct line *ptr = (struct line*)malloc(sizeof(line));
ptr->len = strlen(str);
strcpy((char*)(ptr + 1), str);
printf("start: %p\n\n", (char*)ptr);
printf("(char*)(ptr+1)address: %p\n", (char*)(ptr+1));
printf("(char*)(ptr+1): %s\n\n", (char*)(ptr+1));
printf("&(ptr->contents): %p\n", &(ptr->contents));
printf("ptr->contents: %s\n\n", ptr->contents);
return 0;
}
输出结果为:
start: 0x55f1250482a0
(char*)(ptr+1)address: 0x55f1250482a4
(char*)(ptr+1): hello world
&(ptr->contents): 0x55f1250482a4
ptr->contents: hello world
注意:把上面的
struct line ptr = (struct line)malloc(sizeof(line) + strlen(str) + 1);
换成
struct line ptr = (struct line)malloc(sizeof(line));
程序照样可以work,但是这是有风险的,因为没有分配的strlen(str)+1长度的空间谁也不知道会发生什么。
start: 0x561f821812a0
(char*)(ptr+1)address: 0x561f821812a4
(char*)(ptr+1): hello world
&(ptr->contents): 0x561f821812a4
ptr->contents: hello world
甚至你不用malloc,而是用line l把它定义在栈上,程序照样可以work!。这个风险更大,因为栈空间是有限的,没有分配的空间一是别人可能会写,而是如果这部分空间很大的话可能超出栈的范围。文章来源:https://www.toymoban.com/news/detail-406478.html
/******************************************************************************
Online C Compiler.
Code, Compile, Run and Debug C program online.
Write your code in this editor and press "Run" button to compile and execute it.
*******************************************************************************/
#include <stdio.h>
#include <malloc.h>
#include <string.h>
typedef struct line
{
int len;
char contents[0];
}line;
int main(int argc, char **argv)
{
char str[] = "hello world";
line l;
printf("sizeof(l)=%lu\n", sizeof(l));
l.len = strlen(str);
strcpy((char*)(&l + 1), str);
printf("start: %p\n", (char*)&l);
printf("(char*)(&l+1) address: %p\n", (char*)(&l+1));
printf("(char*)(&l+1): %s\n", (char*)(&l+1));
printf("&(l.contents): %p\n", &(l.contents));
printf("l.contents: %s\n", l.contents);
printf("l.contents: %c%c%c%c%c%c%c%c%c%c%c\n", l.contents[0], l.contents[1], l.contents[2], l.contents[3], l.contents[4], l.contents[5], l.contents[6], l.contents[7], l.contents[8], l.contents[9], l.contents[10]);
return 0;
}
运行结果为:
sizeof(l)=4
start: 0x7ffcfc72cd98
(char*)(&l+1) address: 0x7ffcfc72cd9c
(char*)(&l+1): hello world
&(l.contents): 0x7ffcfc72cd9c
l.contents: hello world
l.contents: hello world文章来源地址https://www.toymoban.com/news/detail-406478.html
到了这里,关于谈谈C/C++的指针,数组和可变长数组的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!