衔接上文:C语言高级特性 第一步:了解指针1
4.3、指针类型转换
指针类型转换是C语言中一个非常重要的概念,它允许程序员将一个指针强制转换为另一个类型的指针。指针类型转换的主要目的是允许程序员在不同类型的数据之间进行转换,以便更方便地进行计算或访问内存中的数据。
在C语言中,指针类型转换的语法如下:
(type *) pointer
其中,type是你想要将指针转换成的类型,pointer是你想要转换的指针。比如,有一个指向整数的指针,但你想将它转换为指向字符的指针,你可以这样写:
int *ip;
char *cp;
cp = (char *) ip;
在这个例子中,首先声明了一个指向整数的指针ip,然后声明了一个指向字符的指针cp。我们将ip强制转换为char类型,并将结果赋给cp。
需要注意的是,指针类型转换是非常危险的,因为它可能会导致数据类型不匹配,从而引发未定义的行为。因此,在进行指针类型转换时,必须非常小心并确保类型转换是安全的。
另外,需要注意的是,指针类型转换是C语言中唯一允许将void指针转换为其他类型指针的方法。比如,有一个指向void的指针,你可以这样将它转换为指向int的指针:
void *vp;
int *ip;
ip = (int *) vp;
在这个例子中,程序将vp强制转换为int类型,并将结果赋给ip。
以下是三个关于指针类型转换的完整例子:
1. 指针类型转换的基础用法
#include <stdio.h>
int main(int argc, char const *argv[])
{
int i = 10;
// 声明指向整型变量i的指针ip,并将其初始化为i的地址
int *ip = &i;
// 声明指向字符的指针cp,并将ip强制转换成指向字符的指针
char *cp = (char *)ip;
// 输出整型变量i的值
printf("i = %d\n", i);
// 输出指针ip所指向的整型变量的值
printf("*ip = %d\n", *ip);
// 输出指针cp所指向的字符的值,由于强制转换,这里会访问到i的内存字节
printf("*cp = %d\n", *cp);
return 0;
}
在Linux上所观察到的运行结果:
在这个例子中,首先声明了一个整型变量i和一个指向整型变量i的指针ip,然后将ip强制转换成了一个指向字符的指针cp,在输出结果中,可以看到i、ip和cp的值分别为10、10和10。这是因为在内存中,整型变量i、指向整型变量i的指针ip所占用的字节和指向字符的指针cp所占用的字节是相同的,因此通过cp指针可以访问到i的值。
2. 指针类型转换时的类型安全问题
#include <stdio.h>
void func(int *p)
{
printf("func: *p=%d\n", *p);
}
int main(int argc, char const *argv[])
{
short s = 10;
short *sp = &s;
printf("Before: *sp=%d\n", *sp);
func((int *)sp);
printf("After: *sp=%d\n", *sp);
return 0;
}
在Linux上所观察到的运行结果:
在这个程序中定义了一个short类型的变量s
,并且定义了一个指向s
的指针变量sp
,然后将sp
强制转换为int类型的指针变量,并将其作为参数传递给了func
函数。
在func
函数中打印了指针指向的值*p
,但是由于传递的是一个short类型的指针,而在函数中将其强制转换为int类型的指针,因此打印出来的值是不确定的,可能会出现随机的、不正确的值。这就是上述程序中打印出的func: *p=-2073034742
这样的结果。
需要注意的是,强制类型转换是一种危险的操作,可能会导致指针指向不正确的内存地址,从而产生意想不到的结果。在使用指针时,应该尽量避免进行类型转换,或者在转换之前进行必要的检查和处理,确保转换后的指针指向的内存地址是合法的。
另外,在这个程序中,由于没有修改指针指向的内存地址,因此在传参前后指针指向的值是一样的,即输出结果中的Before: *sp=10
和After: *sp=10
是相同的。但是如果在函数中修改了指针指向的内存地址对应的值,那么在函数外部输出指针指向的值时,就会发生变化。因此,在使用指针时,需要注意指针指向的内存地址是否被修改,以及是否会对程序产生影响。
- 指针类型转换时的void指针用法
#include <stdio.h>
int main(int argc, char const *argv[])
{
int i = 10;
void *vp = &i;
int *ip = (int *)vp;
printf("i = %d\n", i);
printf("*ip = %d\n", *ip);
return 0;
}
在Linux上所观察到的运行结果:
在这个例子中,首先声明了一个整型变量i和一个指向void的指针vp,然后将vp强制转换成了一个指向整型变量的指针ip。在输出结果中,可以看到i和*ip的值分别为10和10,这说明成功地将void指针vp转换成了指向整型变量的指针ip。需要注意的是,这种用法只适用于void指针的转换,其他类型指针转换时需要注意类型安全问题。
“void *”是一种通用指针类型,可以指向任意类型的数据,但是不能直接对其进行指针运算和解引用操作。
在使用“void *”指针时,需要先将其转换为对应类型的指针,然后才能进行操作。
由于“void *”指针不知道指向的是什么类型的数据,因此在进行转换时需要注意转换的正确性。
如果转换为错误的类型指针,就会导致程序出错,例如访问了不属于该指针类型的内存空间,或者进行了错误的类型转换。因此,在使用“void *”指针时,需要谨慎操作,避免出现错误。
4.4、指向指针的指针
指向指针的指针是指一个指针变量存储的是另一个指针变量的地址,从而可以通过一级指针访问到另一个指针变量,进而访问到其所指向的内存地址。指向指针的指针通常用于函数调用中需要修改传入指针变量的情况。
下面是一个例子,演示如何定义和使用指向指针的指针:
#include <stdio.h>
// 定义一个函数,接受一个int类型的指针的指针作为参数
void func(int **p)
{
int val = 20;
*p = &val; // 将指向val的指针赋值给传入的指针变量
}
int main(int argc, char const *argv[])
{
int num = 10;
int *p1 = # // 定义指向num的指针
int **p2 = &p1; // 定义指向指针p1的指针
printf("num = %d\n", num); // 输出num的值
printf("*p1 = %d\n", *p1); // 输出p1指向的内存地址上的值
printf("**p2 = %d\n\n", **p2); // 输出p2指向的内存地址上的值
func(p2); // 调用函数,将p1指向的内存地址修改为val的地址
printf("**p2 = %d\n", **p2); // 输出p2指向的内存地址上的值,应该等于20
return 0;
}
在Linux上运行得到的结果:
在上面的例子中,首先定义了一个int类型的变量num
,然后定义了一个指向num
的指针p1
。接着定义了一个指向指针p1
的指针p2
,即p2
存储了p1
的地址。这样就可以通过p2
来访问p1
所指向的内存地址,进而访问到num
的值。
接下来调用了一个函数func
,该函数接受一个指向指针的指针作为参数。在函数内部定义了一个int类型的变量val
,然后将其地址赋值给传入的指针变量*p
,即p1
指向了val
的地址。这样,在函数返回后,p1
所指向的内存地址就被修改为val
的地址了。
最后,再次输出了**p2
的值,应该等于val
的值,也就是20。
在上面这个例子中,仅是举例说明可以操控指向指针的指针达到修改变量的效果,但需要注意的是,指向已经释放的内存空间的指针是一种未定义行为,这种行为是不可预测的,可能会导致程序出现各种奇怪的错误。因此在实际代码中应该避免这种行为的出现,以确保程序的正确性和稳定性。
4.5、const指针
“const指针”是C语言中的一种指针类型,它是指向常量的指针。常量指的是在程序运行期间无法修改的值,如字面量、表达式的值等,下面来详细了解一下。
const指针是一个指针,其指向的变量是常量,不能通过该指针修改该变量的值。例如:
int a = 10;
const int *p = &a;
这里定义了一个整型变量a
,并将其值赋为10,接着定义了一个指向常量整型变量的指针p
,并将其值赋为变量a
的地址,即&a
。由于p
是指向常量的指针,因此不能通过p
来修改变量a
的值。
可以通过以下方式访问变量a
:
printf("%d",a); // 输出10
通过下面方式访问const指针变量p
:
printf("%d",*p); // 输出10
尝试修改const指针变量p
所指向的变量a
的值:
*p = 20; // 编译错误
由于p
是指向常量的指针,因此不能通过p
来修改变量a
的值。
接下来通过一个完整的代码来说明const指针的使用:
#include <stdio.h>
int main(int argc, char const *argv[])
{
int a = 10;
const int *p = &a;
printf("a = %d\n", a);
printf("*p = %d\n", *p);
// 尝试修改*p的值
*p = 20; // 编译错误
return 0;
}
在Linux中过编译报错:
上面这个例子中定义了一个整型变量a
和一个指向常量整型变量的指针p
,然后使用printf
函数输出变量a
和指针变量p
所指向的变量的值。接着尝试通过指针变量p
来修改变量a
的值,但是编译时会报错。
通过这个例子可以看出const指针的使用方法,以及它的作用。在实际开发中,const指针常用于声明函数参数或返回值,以保证函数不会修改指向的变量的值。
以下再举两个例子辅以说明:
1. 指向常量的指针参数
#include <stdio.h>
void print_array(const int *arr, int size)
{
int i;
for (i = 0; i < size; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
int main(int argc, char const *argv[])
{
int arr[] = {1, 2, 3, 4, 5};
int size = sizeof(arr) / sizeof(arr[0]);
// 调用函数print_array,并把数组arr和数组大小size作为参数传递给函数
print_array(arr, size);
return 0;
}
在Linux中观察到的运行结果:
上面的这个例子中定义了一个函数print_array
,该函数接受一个指向常量整型数组的指针arr
和数组的大小size
作为参数,函数内部只是简单地打印了数组的元素。在print_array
函数内部,由于arr
是指向常量的指针,因此不能通过该指针修改数组的元素。
将函数参数声明为`const int *arr`的好处是可以避免在函数内部对数组进行修改。这样做可以提高代码的可读性和可维护性,因为在函数内部我们不需要关心传递给函数的数组是否会被修改,而可以专注于实现函数的逻辑。
如果不把参数声明为`const int *arr`,则有可能会在函数内部修改传递给函数的数组,这可能会导致程序出错。而如果把参数声明为`const int *arr`,则在函数内部如果试图修改数组的元素,编译器会报错,从而避免了这种情况的发生。
另外,将函数参数声明为`const int *arr`还可以增加程序的安全性。因为如果其他部分的代码试图修改传递给函数的数组,编译器也会报错,从而防止了程序中可能存在的潜在错误。
2. 指向常量的指针返回值
#include <stdio.h>
// 返回一个数组中的最大值
const int *max(const int *arr, int size)
{
const int *max = arr; // 初始化最大值为数组的第一个元素
int i;
for (i = 1; i < size; i++)
{
if (arr[i] > *max)
{
max = &arr[i];
}
}
return max;
}
int main(int argc, char const *argv[])
{
int arr[] = {1, 4, 3, 6, 5};
int size = sizeof(arr) / sizeof(arr[0]);
// 调用函数max,并把数组arr和数组大小size作为参数传递给函数
const int *p = max(arr, size);
// 输出最大值
printf("\nmax = %d\n", *p);
return 0;
}
在Linux中过编译可以看到这样的结果:
在上面的例子中,定义了一个函数max
,该函数接受一个指向常量整型数组的指针arr
和数组的大小size
作为参数,函数内部在数组中查找最大值,并返回该最大值的指针。由于max
函数返回的是指向常量的指针,因此函数外部不能通过该指针修改数组的元素。
在main
函数中定义了一个整型数组arr
,并把它作为参数传递给函数max
。在调用max
函数时,由于函数返回的是指向常量的指针,因此不能通过该指针修改数组的元素,最后输出最大值。
在这两个例子中可以看到const指针的灵活使用,既可以作为函数参数,也可以作为函数返回值,以保证函数不会修改指向的变量的值。
4.6、指针和结构体的关系
指针和结构体是C语言中非常重要的概念,结构体是一种自定义的数据类型,它可以包含多个不同类型的成员变量,而指针则是一种存储变量内存地址的数据类型。在C语言中,可以使用指针来访问结构体中的成员变量,也可以使用指针来传递结构体对象给函数进行操作。
下面是一个例子,演示了如何定义结构体、创建结构体对象、使用指针来访问结构体成员变量以及将结构体对象传递给函数进行操作。
#include <stdio.h>
#include <string.h>
// 定义一个结构体类型
struct person
{
char name[20];
int age;
float height;
};
// 使用指针访问结构体的成员变量
void print_person(struct person *p)
{
printf("name = %s, age = %d, height = %.2f\n", p->name, p->age, p->height);
}
// 通过指针修改结构体的成员变量
void modify_person(struct person *p, char *name, int age, float height)
{
strcpy(p->name, name);
p->age = age;
p->height = height;
}
int main(int argc, char const *argv[])
{
// 创建一个person类型的结构体对象
struct person tom = {"Tom", 20, 1.80};
// 使用指针访问结构体的成员变量
struct person *p_tom = &tom;
print_person(p_tom);
// 通过指针修改结构体的成员变量
modify_person(p_tom, "Tom Smith", 25, 1.85);
print_person(p_tom);
return 0;
}
在Linux中可以观察到的运行结果:
在上面的例子中,首先定义了一个结构体类型person
,它包含三个成员变量:姓名、年龄和身高。然后创建了一个person
类型的结构体对象tom
,并对其进行初始化。接着使用指针p_tom
来访问tom
对象的成员变量,使用print_person()
函数来输出tom
对象的信息。然后又使用指针p_tom
来修改tom
对象的成员变量,使用modify_person()
函数来修改tom
对象的信息。最后再次使用print_person()
函数来输出tom
对象的信息,可以看到tom
对象的信息已经被修改了。
通过上面的例子,可以看到指针和结构体之间的关系非常密切,使用指针可以方便地访问和修改结构体的成员变量,也可以将结构体对象传递给函数进行操作。
4.7、动态内存分配
本身这部分放在专门的章节会好点,但既然涉及到了指针,那么这里也放点内容吧!
动态内存分配是指在程序运行时,根据需要动态地分配内存空间,而不是在编译时就分配好固定大小的内存空间。动态内存分配可以在程序运行时根据实际需要分配内存,提高了程序的灵活性和效率。
在C语言中,动态内存分配是通过malloc()、calloc()、realloc()等函数实现的。这些函数都在stdlib.h头文件中声明,需要在使用前包含该头文件。
1. malloc()
malloc()函数用于分配指定大小的内存空间,并返回指向该内存空间的指针。其函数原型如下:
void *malloc(size_t size);
其中,size参数表示要分配的内存空间的大小,单位是字节。如果分配成功,malloc()函数返回指向该内存空间的指针;如果分配失败,malloc()函数返回NULL指针。
下面是一个使用malloc()函数分配动态内存的例子:
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char const *argv[])
{
int *p = NULL; // 定义一个指向int类型的指针
int n = 5; // 定义需要分配的数组大小
// 动态分配数组空间
p = (int *)malloc(n * sizeof(int));
if (p == NULL)
{
printf("Out of memory!\n");
exit(1);
}
// 初始化数组
int i;
for (i = 0; i < n; i++)
{
p[i] = i;
}
// 输出数组
for (i = 0; i < n; i++)
{
printf("%d ", p[i]);
}
printf("\n");
// 释放内存空间
free(p);
return 0;
}
在Linux中过编译并运行:
在这个例子中,先定义了一个指向int类型的指针p
,然后定义了需要分配的数组大小n
。接着使用malloc()
函数动态分配了n
个int类型的内存空间,并将返回的指针赋值给p
。如果分配成功,则p
指向了一段连续的内存空间,可以像普通数组一样使用。如果分配失败,则malloc()
函数返回NULL指针,这时就需要在程序中进行判断并采取相应的措施。
- calloc()
calloc()函数与malloc()函数类似,也用于分配指定大小的内存空间。但与malloc()函数不同的是,calloc()函数会将分配的内存空间全部初始化为0。其函数原型如下:
void *calloc(size_t nmemb, size_t size);
其中,nmemb参数表示要分配的元素个数,size参数表示每个元素的大小,单位是字节。如果分配成功,calloc()函数返回指向该内存空间的指针;如果分配失败,calloc()函数返回NULL指针。
下面是一个使用calloc()函数分配动态内存的例子:
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char const *argv[])
{
int *p = NULL; // 定义一个指向int类型的指针
int n = 5; // 定义需要分配的数组大小
// 动态分配数组空间
p = (int *)calloc(n, sizeof(int));
if (p == NULL) {
printf("Out of memory!\n");
exit(1);
}
// 输出数组
int i;
for (i = 0; i < n; i++) {
printf("%d ", p[i]);
}
printf("\n");
// 释放内存空间
free(p);
return 0;
}
在Linux中显示的运行结果:
在这个例子中,先定义了一个指向int类型的指针p
,然后定义了需要分配的数组大小n
。接着使用calloc()
函数动态分配了n
个int类型的内存空间,并将返回的指针赋值给p
。由于calloc()函数会将分配的内存空间全部初始化为0,所以输出数组时,所有元素的值都是0。最后,使用free()
函数释放了动态分配的内存空间。
- realloc()
realloc()函数用于重新分配已分配内存空间的大小,并返回指向该内存空间的指针。其函数原型如下:
void *realloc(void *ptr, size_t size);
其中,ptr参数表示原分配的内存空间的指针,size参数表示重新分配的内存空间的大小,单位是字节。如果分配成功,realloc()函数返回指向该内存空间的指针;如果分配失败,realloc()函数返回NULL指针。
下面是一个使用realloc()函数重新分配动态内存的例子:
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char const *argv[])
{
int n = 5; // 定义需要分配的数组大小
int *p = (int *)malloc(n * sizeof(int));
// 动态分配数组空间
if (p == NULL)
{
printf("Out of memory!\n");
exit(1);
}
// 输出数组
int i;
for (i = 0; i < n; i++)
{
printf("%d ", p[i]);
}
printf("\n");
// 重新分配数组空间
n = 10;
p = (int *)realloc(p, n * sizeof(int));
if (p == NULL)
{
printf("Out of memory!\n");
exit(1);
}
// 初始化新分配的数组元素
for (i = 5; i < n; i++)
{
p[i] = i;
}
// 输出数组
for (i = 0; i < n; i++)
{
printf("%d ", p[i]);
}
printf("\n");
// 释放内存空间
free(p);
return 0;
}
在Linux中编译运行得到的结果:
在这个例子中,首先使用malloc()
函数动态分配了5个int类型的内存空间,并将返回的指针赋值给p
。接着输出了数组元素的值,并使用realloc()
函数重新分配内存空间,将数组的大小增加到10。如果分配成功,则p
指向了一段新的连续内存空间,我们需要重新初始化新增的数组元素。最后,再次输出数组元素的值,并使用free()
函数释放了动态分配的内存空间。
总之,动态内存分配是C语言中非常重要的一个特性,它可以提供灵活的内存管理方式,使程序更加高效、可靠。但是,在使用动态内存分配的过程中,需要注意内存泄漏、野指针等问题,以免造成程序异常。
文章版本:v1.0