一、指针
1.1 指针的基本介绍
指针的重要性:指针是C语言的灵魂。
指针,也就是内存的地址;所谓指针变量,也就是保存了内存地址的变量。
地址:
是从内存单元的编号 ,是从0开始的非负整数 ,如:0–FFFFFFFF【4G-1】
指针:
指针就是地址 ,地址就是指针 。指针变量是存放在内存单元地址的变量, 指针的本质是一个操作受限的非负整数
基本概念
int i = 0;
int *p = &i; //等价于int *p; p = &i;
详解这两步操作:
(1)p 存放了 i 的地址,所以我们说 p 指向了 i
(2)p 和 i 是完全不同的两个变量,修改其中的任意一个变量的值不影响另一个变量的值
(3)p 指向 i, *p 就是i变量本身。更形象的说所有出现 *p 的地方都可以换成 i,所有出现 i 的地方都可以换成 *p
注意:
指针变量也是变量,只不过它存放的不能算是内存单元的内存地址
普通变量前不能加 * ,常量和表达式前不能加&
实例:
#include<stdio.h>
int main()
{
int *p;//p是个变量名,int * 表示该p变量只能存储int类型变量的地址
int i = 10;
int j;
//j = *p;//p没保存地址,那么*p就不知道指向哪个地址
p = &i;
j = *p; //等价于 j = i;
//p = 10 //error
printf("i = %d, j = %d, *p = %d\n", i, j, *p);
return 0;
}
内存分析图:
1.2 指针的运算(++,–,+,-)
指针是一个用数值表示的地址。可以对指针执行算术运算。
数组名
一维数组名是个指针常量, 它存放的是一维数组第一个元素的地址, 它的值不能被改变,一维数组名指向的是数组的第一个元素
下标和指针的关系
a[i] <<==>> *(a+i)
假设指针变量的名字为p, 则 p+i 的值是p+i*(p所指向的变量所占的字节数)
实例:
#include<stdio.h>
int main()
{
int arr[5] = {1,2,3,4,5};
int i, *p; //p是一个int* 指针
p = arr;//p指向了arr数组的首地址
for(i = 0;i < 5;i++)
{
printf("arr[%d] 地址=%p\n",i,p);
printf("存储值:arr[%d]=%d\n",i,*p);
p++;
}
return 0;
}
内存结构图:
● 数组在内存中是连续分布的
● 当对指针进行++时,指针会按照它指向的数据类型字节数大小增加,比如int* 指针,每++,就增加 4 个字
节。
● 所有指针变量只占4个字节,用第一个字节的地址表示整个变量的地址。
1.3 指针数组
要让数组的元素指向 int 或其他 数据类型的地址(指针)。可以使用指针数组。
指针数组定义:
数据类型 *指针数组名[大小]
如:int *arr[3];
arr 声明为一个指针数组 。
arr 由 3 个整数指针组成。因此,arr 中的每个元素,都是一个指向 int 值的指针。
比如:
#include<stdio.h>
int main()
{
int arr[3] = {100,200,300};
int i, *p[3]; //p是一个int* 指针
for(i = 0;i < 3;i++)
{
p[i] = &arr[i]; //赋值为整数的地址
}
for(i = 0;i < 3;i++)//指针数组获取各个值
{
printf("arr[%d]=%d p[%d]地址=%p\n",i,*p[i],i,&p[i]);
}
return 0;
}
内存布局:
1.4 指向指针的指针(多重指针)
指向指针的指针是一种多级间接寻址的形式,或者说是一个指针链。通常,一个指针包含一个变量的地址。当我们定义一个指向指针的指针时,第一个指针包含了第二个指针的地址,第二个指针指向包含实际值的位置。
(1)一个指向指针的指针变量必须如下声明,即在变量名前放置两个星号。例如,声明了一个指向int 类型指针的指针:int **ptr;
(2)当一个目标值被一个指针间接指向到另一个指针时,访问这个值需要使用两个星号运算符,比如**ptr
实例:
#include<stdio.h>
int main()
{
int var;
int *p;//一级指针
int **pp;//二级指针
var = 1000;
p = &var;//var变量的地址赋值给p
pp = &p;//表示将p存放的地址,赋值给pp
printf("var的地址=%p var = %d\n",&var,var);
printf("p本身的地址=%p p存放的地址 = %d *p = %d\n",&p,p,*p);
printf("pp本身的地址=%p pp存放的地址 = %d *pp = %d\n",&pp,pp,*pp);
return 0;
}
内存结构图:
1.4 传递指针(地址)给函数
当函数的形参类型是指针类型时,使用该函数时,需要传递指针,或者地址,或者数组给该形参。
如何通过被调函数修改主调函数中普通变量的值?
- 实参为相关变量的地址
- 形参为以该变量的类型为类型的指针变量
- 在被调函数中通过 *形参变量名 的方式就可以修改主函数中的变量的值
实例1(传地址或指针给指针变量):
#include<stdio.h>
void f(int *p) //不是定义了一个名字叫做*p的形参,而是定义了一个形参,该形参名字叫做p,它的类型是int *
{
*p += 1;
}
int main()
{
int i = 10;
int *p = &i;//将i的地址赋值给p
f(&i);//传地址
printf("i = %d\n", i);//i=11
f(p);//传指针
printf("i = %d\n", i);//i=12
return 0;
}
内存分析:
实例2(传数组给指针变量):
#include <stdio.h>
/* 函数声明 */
double getAverage(int *arr, int size); //
int main ()
{
/* 带有 5 个元素的整型数组 */
int balance[5] = {1000, 2, 3, 17, 50};
double avg;
/* 传递一个指向数组的指针作为参数 */
avg = getAverage( balance, 5 ) ;
/* 输出返回值 */
printf("Average value is: %f\n", avg );
return 0;
}
//说明
//1. arr 是一个指针,
double getAverage(int *arr, int size)
{
int i, sum = 0;
double avg;
for (i = 0; i < size; ++i)
{
// arr[0] = arr + 0
// arr[1] = arr + 1个int字节(4)
// arr[2] = arr + 2个int字节(8)
sum += arr[i];// arr[0] =>数组第一个元素的地址
printf("\n arr存放的地址=%p \n", arr);
}
avg = (double)sum / size;
return avg;
}
在前面说过 a[i] <<==>> *(a+i)
,所以上面的 arr[i]
还可以写成 *(arr + i)
。也可以这样:
sum += *arr;
arr++; // 指针的++运算, 会对arr 存放的地址做修改
1.5 返回指针的函数
允许函数的返回值是一个指针(地址),这样的函数称为指针函数。
实例:
#include <stdio.h>
#include <string.h>
char *strlong(char *str1, char *str2){ //函数返回的char * (指针)
printf("\nstr1的长度:%d str2的长度:%d", strlen(str1), strlen(str2));
if(strlen(str1) >= strlen(str2)){
return str1;
}else{
return str2;
}
}
int main(){
char str1[30], str2[30], *str; // str 是一个指针类型,指向一个字符串
printf("\n请输入第1个字符串:");
gets(str1);
printf("\n请输入第2个字符串:");
gets(str2);
str = strlong(str1, str2);
printf("\nLonger string: %s \n", str);
return 0;
}
注意事项:
- 用指针作为函数返回值时需要注意,函数运行结束后会销毁在它内部定义的所有局部数据,包括局部变量、局部数组和形式参数,函数返回的指针不能指向这些数据。
- 函数运行结束后会销毁该函数所有的局部数据,这里所谓的销毁并不是将局部数据所占用的内存全部清零,而是程序放弃对它的使用权限,后面的代码可以使用这块内存
- C语言不支持在调用函数时返回局部变量的地址,如果确实有这样的需求,需要定义局部变量为static变量
1.6 函数指针(指向函数的指针)
- 一个函数总是占用一段连续的内存区域,函数名在表达式中有时也会被转换为该函数所在内存区域的首地址,这和数组名非常类似。
- 把函数的这个首地址(或称入口地址)赋予一个指针变量,使指针变量指向函数所在的内存区域,然后通过指针变量就可以找到并调用该函数。这种指针就是函数指针。
函数指针定义:
returnType (* pointerName)(param list);
- returnType 为函数指针指向的函数返回值类型
- pointerName 为函数指针名称
- param list为函数指针指向的函数的参数列表
- 参数列表中可以同时给出参数的类型和名称,也可以只给出参数的类型,省略参数的名称
- 注意( )的优先级高于*,第一个括号不能省略,如果写作
returnType *pointerName(param list);
就成了函数原型,它表明函数的返回值类型为returnType*
实例(用函数指针来实现对函数的调用,返回两个整数中最大值):
#include <stdio.h>
int max(int a,int b)
{
return a>b?a:b;
}
int main(){
int x,y,maxVal;
//说明函数指针
//1.函数指针的名字pmax
//2. int表示该函数指针指向的函数是返回int类型
//3. (int, int)表示该函数指针指向的函数形参是接收两个int
//4.在定义函数指针时,也可以写上形参名int (*pmax)(int x, int y) = max;
int (*pmax)(int , int)= max;
printf("Input two numbers:");
scanf("%d %d", &x, &y);
// (*pmax)(x, y)通过函数指针去调用函数max
maxVal = (*pmax)(x, y);
printf("Max value: %d pmax=%p pmax本身的地址=%p\n", maxVal, pmax, &pmax);
return 0;
}
内存结构图分析:
1.7 指针的注意事项和细节
- 指针变量存放的是地址,从这个角度看指针的本质就是地址。
- 变 量声明的时候,如果没有确切的地址赋值,为指针变量赋一个NULL值是好的编程习惯。
- 赋为NULL值的指针被称为空指针,NULL指针是一个定义在标准库<stdio.h>中 的值为零的常量
#define NULL 0
二、结构体
2.1 为什么要有结构体
比如有一个学生有学号,名字,年龄等属性,多个学生如果按常规变量赋值就会使得代码冗余,那么就要把其各个属性抽象出来,形成一个新的类型,所以结构体诞生了。也就是:为了表示一些复杂的数据,而普通的基本类型变量无法满足要求。
2.2 什么是结构体
结构体是用户根据实际需要自己定义的复合数据类型。其实结构体就是面向对象语言中类,只是结构体中没有方法,并且最后有分号。
class Student
{
int sid;
String name;
int sage;
void inputStudent()
{
}
void showStudent()
{
}
}
struct Student
{
int sid;
char *name;
int sage;
}; //分号不能省
2.2.1 结构体和结构体变量的区别与联系
- 结构体是自定义的数据类型,表示的是一种数据类型.
- 结构体变量代表 一个具体变量,好比:
int numl; //int 是数据类型,而num1是一个具体的int变量
struct Student stu;// Student 是结构体数据类型,而 stu是一个Cat变量
2.3 如何使用结构体
两种方式:
struct Student st = {1000,"zhangsan",20};
struct Student *pst = &st;
- st.sid
- pst->sid
实际使用如下:
#include <stdio.h>
#include<string.h>
struct Student
{
int sid;
char *name;//名字,用指针,指向一个字符串
int age;
};//分号不能省
int main(){
struct Student st = {1000,"zhangsan",20};
//第一种方式
/*
st.sid = 99;
//st.name = "lisi"; //error
st.name = "lisi";
st.age = 19;
*/
//第二种方式(常用)
struct Student *pst;
pst = &st;
pst->sid = 99; //pst->sid 等价于 (*pst).sid, 而(*pst).sid等价于st.sid,所以pst->sid等价于st.sid
pst->name = "wangwu";
pst->age = 21;
printf("%d %s %d \n",st.sid,st.name,st.age);
return 0;
}
2.4 注意事项
- 结构体变量不能加减乘除,但可以相互赋值
- 普通结构体变量和结构体指针变量作为函数传参问题
#include <stdio.h>
#include<string.h>
typedef struct Student
{
int sid;
char *name;//名字,用指针,指向一个字符串
int age;
}Student;//分号不能省
void f(Student *pst)
{
pst->sid = 88;
pst->name = "mayun";
pst->age = 18;
}
//这种方式消耗内存,耗时间,不推荐
void g(Student st)
{
printf("%d %s %d \n",st.sid,st.name,st.age);
}
void g2(Student *pst)
{
printf("%d %s %d \n",pst->sid,pst->name,pst->age);
}
int main(){
Student st;
f(&st);
//g(st);
g2(&st);
return 0;
}
typedef的用法
typedef struct Student
{
int sid;
char *name;
int age;
}Student,*pStudent;//等价于Student代表了struct Student ,pStudent代表了struct Student *
用法:
#include <stdio.h>
#include<string.h>
typedef struct Student
{
int sid;
char *name;
int age;
}Student,*pStudent;//等价于Student代表了struct Student ,pStudent代表了struct Student *
int main(){
Student st;//struct Student st;
pStudent ps = &st;//struct Student *st = &st;
ps->sid = 999;
printf("%d\n",ps->sid);
return 0;
}
三、动态内存分配与释放
3.1 不同数据在内存中的分配
- 全局变量一一内存中的静态存储区
- 非静态的局部变量——内存中的动态存储区——stack栈
- 临时使用的数据——建立动态内存分配区域,需要时随时开辟,不需要时及时释放——heap 堆
- 根据需要向系统申请所需大小的空间,由于未在声明部分定义其为变量或者数组,不能通过变量名或者数组名
来引用这些数据,只能通过指针来引用
3.2 内存动态分配相关函数
头文件 #include <stdlib.h>
声明了四个关于内存动态分配的函数,下面我只介绍最常用的函数malloc和free。
-
函数原型
void * malloc (usigned int size)
● 作用:在内存的动态存储区(堆区)中分配一个长度为size的连续空间。
● 形参size的类型为无符号整型,函数返回值是所分配区域的第-一个字节的地址,即此函数是一个指针型函数,返回的指针指向该分配域的开头位置(函数首地址)。
●malloc(100);
开辟100字节的临时空间,返回值为其第一个字节的地址 -
函数原型:
void free (void *p)
● 作用:释放变量p所指向的动态空间,使这部分空间能重新被其他变量使用。
● p 是最近一次调用calloc或malloc函数时的函数返回值
● free函数无返回值
●free(p);
//释放p所指向的已分配的动态空间
C99标准把以上malloc,calloc,realloc函数的基类型定为void类型,这种指针称为无类型指针(typeless pointer) ,即不指向哪一种具体的类型数据,只表示用来指向一个抽象的类型的数据,即仅提供一个纯地址,而不能指向任何具体的对象。
代码示例:
#include <stdio.h>
#include<stdlib.h>
int main(){
int a[5] = {4,10,2,8,6};//静态分配内存
int len;
printf("请输入你要分配的数组长度:len = ");
scanf("%d",&len);
// 在堆区开辟-一个5*4的空间,并将地址(void*) ,转成(int*),赋给pArr
int *pArr = (int *)malloc(sizeof(int) * len);
/*
*pArr = 4; //类似于a[0] = 4;
pArr[1] = 10; //类似于a[1] = 10;
printf("%d %d\n",*pArr,pArr[1]);//4 10
*/
//pArr完全可以当做普通数组来使用
for(int i = 0;i < len;i++)
{
scanf("%d",&pArr[i]);
}
for(i = 0;i < len;i++)
{
printf("%d\n",*(pArr + i));
}
free(pArr); //把pArr所代表的动态分配的20个字节内存释放
return 0;
}
内存结构图:
3.3 动态分配内存注意事项
- 避免分配大量的小内存块。分配堆上的内存有一些系统开销,所以分配许多小的内存块比分配几个大内存块的系统开销大
- 仅在需要时分配内存。只要使用完堆上的内存块,就需要及时释放它(如果使用动态分配内存,需要遵守原则:(谁分配,谁释放),否则可能出现内存泄漏
- 总是确保释放已分配的内存。在编写分配内存的代码时,就要确定在代码的什么地方释放内存
- 在释放内存之前, 确保不会无意中覆盖堆上已分配的内存地址,否则程序就会出现内存泄漏。在循环中分配内存时,要特别小心