C进阶4
▲二维数组参数中第一维的参数可以省略
void f(int a[5])<==>void f(int a[])<==>void f(int* a)
void g(int a[3][3])<==>void g(int a[][3])<==> void g(int (*a)[3])
▲等价关系:
一位数组float a[5] 指针float* a
指针数组int* a[5] 指针的指针int** a
二维数组char a[3][4] 数组的指针char (*a)[4]
▲
C语言中无法向一个函数传递任意的多维数组
必须提供第一维之外的所有维长度;第一维之外的维度信息用于完成指针运算;
N维数组的本质是一维数组,元素是N—1维的数组;对于多维数组只有第一维信息可变。
▲
#include <stdio.h>
void access(int a[][3], int row)
{
int col = sizeof(*a) / sizeof(int);
int i = 0;
int j = 0;
printf("sizeof(a) = %d\n", sizeof(a));
printf("sizeof(*a) = %d\n", sizeof(*a));
for(i=0; i<row; i++)
{
for(j=0; j<col; j++)
{
printf("%d\n", a[i][j]);
}
}
printf("\n");
}
void access_ex(int b[][2][3], int n)
{
int i = 0;
int j = 0;
int k = 0;
printf("sizeof(b) = %d\n", sizeof(b));
printf("sizeof(*b) = %d\n", sizeof(*b));
for(i=0; i<n; i++)
{
for(j=0; j<2; j++)
{
for(k=0; k<3; k++)
{
printf("%d\n", b[i][j][k]);
}
}
}
printf("\n");
}
int main()
{
int a[3][3] = {{0, 1, 2}, {3, 4, 5}, {6, 7, 8}};
int aa[2][2] = {0};
int b[1][2][3] = {0};
access(a, 3);
access(aa, 2);
access_ex(b, 1);
access_ex(aa, 2);
return 0;
}
//------------------------------------------------
如何使用C语言直接跳转到某个固定的地址开始执行?
通过函数指针。
C语言中函数有自己特定的类型,由返回值、参数类型和参数个数决定。
int add(int i,int j)的类型为int (int,int)
C语言中通过typede为函数类型重命名
typedef type name(parameter list)
例:typedef int f(int,int);
typedef void p(int);
▲
#include <stdio.h>
typedef int(FUNC)(int);
int test(int i)
{
return i * i;
}
void f()
{
printf("Call f()...\n");
}
int main()
{
FUNC* pt = test;
void(*pf)() = &f;
printf("pf = %p\n", pf);
printf("f = %p\n", f);
printf("&f = %p\n", &f);
pf();
(*pf)();
printf("Function pointer call: %d\n", pt(2));
return 0;
}
回调函数是利用函数指针实现的一种调用机制。
函数指针用于指向一个函数,函数名是执行函数体的入口地址;可通过
函数类型定义函数指针:FuncType* pointer;也可以type (*pointer)(parameter list);
pointer为函数指针变量名,type为所指函数的返回值类型,parameter list为所指函数的参数类型列表。
#include <stdio.h>
typedef int(*Weapon)(int);
void fight(Weapon wp, int arg)
{
int result = 0;
printf("Fight boss!\n");
result = wp(arg);
printf("Boss loss: %d\n", result);
}
int knife(int n)
{
int ret = 0;
int i = 0;
for(i=0; i<n; i++)
{
printf("Knife attack: %d\n", 1);
ret++;
}
return ret;
}
int sword(int n)
{
int ret = 0;
int i = 0;
for(i=0; i<n; i++)
{
printf("Sword attack: %d\n", 5);
ret += 5;
}
return ret;
}
int gun(int n)
{
int ret = 0;
int i = 0;
for(i=0; i<n; i++)
{
printf("Gun attack: %d\n", 10);
ret += 10;
}
return ret;
}
int main()
{
fight(knife, 3);
fight(sword, 4);
fight(gun, 5);
return 0;
}
//----------------------------------------------
int (*p)(int):p为指针,指向函数,指向的函数有一个int参数,返回值为int。
int (*p1)(int*,int(*f)(int*)):p1为指针,指向函数;指向的函数有int*,f为第二个参数,它是函数指针,
指向的参数时int*,返回值是int,返回值是int。
int (*p2[5])(int*):p2为数组,有5个元素,这5个元素为指针,指向函数,函数类型为int(int*)。
int (*(*p3)[5])(int*):p3为指针,数组指针,指向的数组有5个元素,这5个元素为指针,该指针是函数指针,指向的函数类型为int(int*)
int* (*(*p4)(int*))(int*):p4为指针,函数指针,参数为int*,返回值为指针,函数指针,指向的函数类型int*(int*)
int (*(*p5)(int*))[5]:p5为指针,指向函数,函数指针参数为int*,返回值为指针,指向数组,指向的数组类型为int[5]
typedef int(ArrayType)[5];
typedef ArrayType*(FuncType)(int*);
FunType* p5;
//------------------------------------------
动态内存分配
malloc0将返回什么?合法。
malloc0不释放后果?产生内存泄漏。
//-------------------------------------------
堆和栈和静态存储区
栈:主要用于函数调用的使用,不要返回局部变量的地址。
堆:内存的动态申请和归还;堆中被程序申请使用的内存在被主动释放前将一直有效。
静态存储区:全局变量和静态变量
为什么有了栈还需要堆?答:栈上的数据在函数返回后就会被释放掉,无法传递到函数外部,如局部数组。
注:空闲链表法、位图法、对象池法。malloc free。
#include <stdio.h>
int g_v = 1;
static int g_vs = 2;
void f()
{
static int g_vl = 3;
printf("%p\n", &g_vl);
}
int main()
{
printf("%p\n", &g_v);
printf("%p\n", &g_vs);
f();
return 0;
}
注:3个变量的地址是挨着的。
//-------------------------------------------------
程序的内存布局:
▲程序和进程不同:
程序是静态的概念,表现形式为一个可执行文件
进程是动态的概念,程序由操作系统加载运行后得到进程
每个程序可以对应多个进程;每个进程只能对应一个程序。
▲包含脚本代码的文本文件是一种类型的可执行程序吗?如果是,对应什么样的进程呢?
▲堆栈段在程序运行后才正式存在,是程序运行的基础
.bss段存放的是未初始化的全局变量和静态变量
.text段存放的是程序中的可执行代码
.data段保存的是已经初始化了的全局变量和静态变量
.rodata段存放程序中的常量值,如字符串常量
▲
静态存储区通常指程序中的.bss和.data段
只读存储区通常指程序中的.rodata段
局部变量所占空间为栈上的空间
动态空间为堆中的空间
程序可执行代码存放于.text段
问:同是全局变量和静态变量,为什么初始化的和未初始化的保存在不同段中?
C规定,未初始化变量的初值为0,这个清0的操作是由启动代码完成的,还有已初始化变量的初值的设置,也是由启动代
码完成的。为了启动代码的简单化,编译链接器会把已初始化的变量放在同一个段:.data,这个段的映像(包含了各个
变量的初值)保存在“只读数据段”,这样启动代码就可以简单地复制这个映像到 .data 段,所有的已初始化变量就都初
始化了。而未初始化变量也放在同一个段:.bss,启动代码简单地调用 memset 就可以把所有未初始化变量都清0。
//-----------------------------------------------------
内存操作经典问题:
野指针的由来:局部指针变量没有初始化;指针所指向的变量在指针之前被销毁;
使用已经释放过得指针;进行了错误的指针运算;进行了错误的强制类型转换;
基本原则:
绝不返回局部变量和局部数组的地址;任何变量在定义后必须0初始化;
字符数组必须确认0结束符后才能成为字符串;任何使用与内存操作相关的函数必须指定长度信息;
#include <stdio.h>
#include <malloc.h>
int main()
{
int* p1 = (int*)malloc(40);
int* p2 = (int*)1234567; //error
int i = 0;
for(i=0; i<40; i++)
{
*(p1 + i) = 40 - i;
}
free(p1); //并不会将指针置为空
for(i=0; i<40; i++)
{
p1[i] = p2[i]; // 使用已经释放了的内存空间
}
return 0;
}
常见内存错误:
结构体成员指针未初始化;结构体成员指针未分配足够的内存;内存分配成功,但并未初始化;内存操作越界;
内存错误的本质源于指针保存的地址为非法值;指针变量未初始化,保存随机值;指针运算导致内存越界;
内存泄漏源于malloc和free不匹配;
//-----------------------------------------------
函数:
函数的参数在栈上分配空间;函数的实参并没有固定的计算次序;顺序点是C语言中变量修改的最晚时机;
C语言中可以定义参数可变的函数:参数可变函数的实现依赖于stdarg.h头文件
va_list参数集合;va_start标识参数访问的开始;va_end标识参数访问的结束;
●可变参数必须从头到尾按照顺序逐个访问,参数列表中至少要存在一个确定的命名参数,可变参数函数无法确定实际存在的参数的数量,
可变参数函数无法确定参数的实际类型;
注:va_arg中如果指定了错误的类型,结果不可预测。
调用约定指定了函数参数的入栈顺序以及栈的清理方式;可变参数必须顺序的访问,无法直接访问中间的参数值;
#include <stdio.h>
#include <stdarg.h>
float average(int n, ...)
{
va_list args;
int i = 0;
float sum = 0;
va_start(args, n);
for(i=0; i<n; i++)
{
sum += va_arg(args, int);
}
va_end(args);
return sum / n;
}
int main()
{
printf("%f\n", average(5, 1, 2, 3, 4, 5));
printf("%f\n", average(4, 1, 2, 3, 4));
return 0;
}
//----------------------------------------------------