C语言指针与内存,函数中的坑

一 写在开头
1.1 本节内容
内存填充函数memset()中的坑。

1、calloc,malloc 和 alloca的区别;

Linux C语言指针与内存

二 函数原型

答案:

工具与原理

  • 指针
  • 数组
  • 字符串
  • 堆内存与栈内存
  • gdb内存调试工具。
1 /* 来自man memset */
2 #include <string.h>
3 void * memset(void * s, int c, size_t n);

内存区域可以分为栈、堆、静态存储区和常量存储区,局部变量,函数形参,临时变量都是在栈上获得内存的,它们获取的方式都是由编译器自动执行的。
    利用指针,我们可以像汇编语言一样处理内存地址,C 标准函数库提供了许多函数来实现对堆上内存管理,其中包括:malloc函数,free函数,calloc函数和realloc函数。使用这些函数需要包含头文件stdlib.h。
    四个函数之间的有区别,也有联系,我们应该学会把握这种关系,从而编出精炼而高效的程序。
    在说明它们具体含义之前,先简单从字面上加以认识,前3个函数有个共同的特点,就是都带有字符”alloc”,就是”allocate”,”分配”的意 思,也就是给对象分配足够的内存,” calloc()”是”分配内存给多个对象”,” malloc()”是”分配内存给一个对象”,”realloc()”是”重新分配内存”之意。”free()”就比较简单了,”释放”的意思,就是把之 前所分配的内存空间给释放出来。

C语言中指针的基本用法

#include <stdio.h>
void change(int a, int b)
{
    int tmp =a;
    a=b;
    b=tmp;
}

int main()
{
    int a=5;
    int b=3;
    change(a,b);
    printf("num a =%dnnum b =%dn",a,b);
    return 0;
}

上述代码无法实现a,b数值的交换。

改为指针实现代码如下:

#include <stdio.h>
void change(int *a, int *b)
{
    int tmp =*a;
    *a=*b;
    *b=tmp;
}

int main()
{
    int a=5;
    int b=3;
    change(&a,&b);
    printf("num a =%dnnum b =%dn",a,b);
    return 0;
}

3和5可以成功的交换。

需要将实参的地址传到子函数才能改变实参!(&a,&b)

C语言 int未初始化时,初值为随机
int变量未初始化的默认初值,和变量的类型有关。

  • 局部变量,在未初始化情况下,初值为随机值。C规范对该初值并没有做规定,具体实现由编译器决定。如VC/VS等编译器,会将初始值值为0xCCCCCCCC, 而GCC等编译器则是不可预知的随机值。
  • 静态局部变量,即带static修饰的局部变量。
    全局变量和静态全局变量,即定义在函数外,不属于任何一个函数的变量。
    这几种默认初值为0.

功能描述:memset()函数用常量c的值填充由指针s所指向的内存地址空间的前n个字节的内存空间。

void *calloc(size_t nobj, size_t size);

gdb工具的使用

安装gdb工具:apt-get install gdb

gcc -g main.c -o main.out生成可调试版本。
gdb ./main.out

  • l:查看源代码
  • 回车:继续执行上条指令
  • break 行数:设置断点
  • start :单步调试
  • n:执行到下一条语句
  • s:进入函数内部
  • p a:查看a在内存中的情况
  • bt:查看函数堆栈
  • f 1:切换到1号函数
  • q:退出调试
  • p *a(int *a 时 p a 打印出的是a的内存地址,p *a打印的是这个地址里对应的值.P &a 显示a的内存地址空间
  • P &functionname p &函数名,显示函数程序在代码段的内存地址)

*a 取a这个地址的内容
&a 去a这个变量的地址

因为不知道一个指针指向的数据有多大, 所以需要在声明一个指针变量的时候需要明确的类型。

解析:只是传值,只是change的局部变量,是实参的备份。

解决:加个指针,取地址符,实现交换功能。

DESCRIPTION : The memset() function fills the first n bytes of the memory area pointed to by s with the constant byte c.

分配足够的内存给nobj个大小为size的对象组成的数组, 并返回指向所分配区域的第一个字节的指针;若内存不够,则返回NULL. 该空间的初始化大小为0字节.char *p = (char *) calloc(100,sizeof(char));

计算机中数据表示方法:

0x表示十六进制
1个16进制的数字,就可以表示4位二进制数字

  • 计算用二进制
  • 显示用十进制
  • 编程用16进制

 

void *malloc(size_t size);

内存管理

32根地址总线就有2的32次方个状态
内存分配
1byte = 8bit
1字节 = 8进制位

用户程序的内存空间从高到低又划分为:

  • 系统内核
  • 栈(暂时存储首先执行的程序状态)
  • 自由可分配内存(可动态分配内存)
  • 数据段(声明一些全局变量或者声明一些常量)
  • 代码段(程序源代码编译后存放在此)

高位内存空间分配给操作系统内核使用,低位内存空间分配给用户程序使用。
我们编写的函数在编译后存到磁盘,运行程序时,就把源代码编译后的二进制数据加载到内存空间中的代码段中。声明的全局变量或常量放置在数据段。每次调用新的函数,就将新的函数压入栈区。
64位系统中 只有前48位是给程序员使用的。 0x7fffffffffffffff ~ 0x0

参数:
void * s - 指向要被填充的内存空间的首地址
int c

分配足够的内存给大小为size的对象, 并返回指向所分配区域的第一个字节的指针;若内存不够,则返回NULL. 不对分配的空间进行初始化.char *p = (char *)malloc(sizeof(char));

变量与指针的本质

#include <stdio.h>

int global = 0;

int rect (int a,int b)
{
    static int count=0;
    count  ;
    global  ;
    int s=a*b;
    return s;
}

int quadrate(int a)
{
    static int count=0;
    count  ;
    global  ;
    int s = rect(a,a);
    return s;
}

int main()
{
    int a=3;
    int b=4;
    int *pa =&a;
    int *pb =&b;
    int *pglobal =&global;
    int (*pquadrate)(int a)= &quadrate;
    int s = quadrate(a);
    printf("%dn",s);  
}
  • gcc -g main.c -o main.out //加-g生成的main.out才可以用gdb进行调试
  • gdb ./main.out //调试

gdb调试命令:

  • l(list) 列出代码
  • start 开始调试
  • n 单步走
  • s 进入函数
  • p 变量名 输出变量的值
  • bt 查看栈标号
  • f 栈标号 切换栈
  • q 退出gdb
  • 回车 重复执行上一次的命令

变量的本质是什么?

  • 变量名只是一个代号。
  • 变量的本质就是内存。

指针的本质?
指针保存内存的地址。
a = 第五个柜子第二个抽屉。

  • 一个常量
    size_t n - 要被填充的字节数

void *realloc(void *p, size_t size);

操作系统对于内存的管理

栈先声明的地址大,后声明的地址小,与代码段数据段相反。
数据段(data segment)通常是指用来存放程序中已初始化的全局变量的一块内存区域。数据段属于静态内存分配。
32位系统指针占用4个字节, 也就是32个bit,64位系统占用64个bit,也就是8字节。

64位系统下,指针占8个字节,32位 4个字节。

编译器优化代码,把声明时不在一起的同一类型变量,放到一起(某种程度上修改了源码)

如 声明 int a ; float b ; int c; 编译后变量a的地址和c的地址是连在一起的.

CPU在编译的时候对栈内变量的存储地址进行优化,他会将类型相同的变量在连续地址中储存。

地址分配:代码,数据段是从下往上分配(先低地址,后高地址),栈是从上往下分配(先高地址,后低地址)

函数中静态变量,局部变量区别:
局部变量在栈(相对数据段而言的高地址)中,而静态变量在数据段(低地址)中.
所以在多次调用函数时,静态变量不会被重新,初始化. 或者这么说,静态变量的生存周期和数据段相同,局部变量生存时间受调用函数时,所属函数进栈出栈的影响而会重新初始化.

全局变量和静态变量都在数据段中,但静态变量是某个函数特有的.

 

将p所指向的对象的大小改为size个字节.如果新分配的内存比原内存大, 那么原内存的内容保持不变, 增加的空间不进行初始化.如果新分配的内存比原内存小, 那么新内存保持原内存的内容, 增加的空间不进行初始化.返回指向新分配空间的指针; 若内存不够,则返回NULL, 原p指向的内存区不变.
char *p = (char *)malloc(sizeof(char));
p= (char *)realloc(p, 256);

函数指针与指针指向的数据访问

#include <stdio.h>

int global = 0;

int rect (int a,int b)
{
    static int count=0;
    count  ;
    global  ;
    int s=a*b;
    return s;
}

int quadrate(int a)
{
    static int count=0;
    count  ;
    global  ;
    int s = rect(a,a);
    return s;
}

int main()
{
    int a=3;
    int b=4;
    int *pa =&a;
    int *pb =&b;
    int *pglobal =&global;
    int (*pquadrate)(int a)= &quadrate;
    int s = quadrate(a);
    printf("%dn",s);  
}
  • &*p:取变量a的地址(先进行*运算,*p相当于变量a,再进行&运算,&*p就相当于取变量a的地址)
  • *&p:取变量a所在地址的值(先进行&运算,&a相当于取变量a的地址,在执行*运算,*&p相当于取变量a所在地址的值)
  • pa ====》0x7fffffffddfc 内存地址
  • *pa 代表取出0x7fffffffddfc 这个地址代表的值
  • p *pa指找到pa中的数据;
  • p &pa指找到pa本身的地址。

返回值:
memset()函数是有返回值的,从函数原型也可以看出来。memset()函数返回一个指向内存空间s的指针。

void free(void *p);
释放p所指向的内存空间; 当p为NULL时, 不起作用.p必先调用calloc, malloc或realloc.

数组申明的内存排列

  • 0x7fffffffcc78:3
  • 0x7fffffffcc7C:2
  • 0x7fffffffcc78: 3 2
  • 0x7fffffffcc84:i

(gdb)p *0x7fffffffcc6c
$15 = 1

(gdb) p *0x7fffffffcc70
$17 = 10

(gdb) p *0x7fffffffcc74
$18 =100

可以看出数组是按顺序放置元素的。

RETURN VALUE : The memset() function returns a pointer to the memory area s.

值得注意的有以下5点:

指针运算。

指针偏移运算。
p =3;
把指针往下移三格-整型移动12个字节
*p =101;
将p指针所指向的值修改为101
p =&a;
让p再次指向a的地址
p[4] = 101
把指针往下移三格-整型移动12个字节
P[4]不是p往下面移动了4个位置,而是从p开始的地址往后移动4个位置取值,p指向的地址还是不变的

数组其实就是个指针常量,指针是指针变量,常量就是不可更改的

int array[2];
int *pa =array;
pa[0]=1;
pa[1]=10;
pa[2]=100;

 

(1)通过malloc函数得到的堆内存必须使用memset函数来初始化malloc函数分配得到的内存空间是未初始化的。因此,一般在使用该内存空间 时,要调用另一个函数memset来将其初始化为全0,memset函数的声明如下:void * memset (void * p,int c,int n) ;

字符数组和指针字符串

int a;
scanf("%d",&a);

int main()
{
    char str[]="hello";
    char *str2="world";
    char str3[10];
    printf("input the valuen");
    scanf("%s",str3);
    printf("str is %sn",str); 
    printf("str2 is %sn",str2);
    printf("str3 is %sn",str3);
}

因为str3是一个字符数组,

gdb的x命令,可以打印地址中的值

  • x/个数 地址
  • x/6cb 地址: 打印该地址后的6个字符,c:字符形式打印,b:按字节显示

scanf可以将输入存入str或str3,但是不能存入str2
堆和栈内存里才可以写入(预留空间才可写入)
而str2是一个代码段变量。不允许写入。

三 填坑运动
3.1 第一个坑
先举一个memset()正确的应用场景。下面的代码能够很好地运行,完全能够达到目的。

该函数可以将指定的内存空间按字节单位置为指定的字符c,其中,p为要清零的内存空间的首地址,c为要设定的值,n为被操作的内存空间的字节长度。如果要用memset清0,变量c实参要为0。

字符串数组的深入理解

#include <stdio.h>

int main()
{
    char str[]="hello";
    char *str2="world";
    char str3[10];
    printf("input the valuen");
    str[3]='\0';
    scanf("%s",str3);
    printf("str is %sn",str); 
    printf("str2 is %sn",str2);
    printf("str3 is %sn",str3);
}

只会打印出hel,因为prinf打印以/0为结束。

char str1[] = "Hello";
char str3[5];
str1 = "HelloWorld";

很有意思 :

  • 当 str1 声明时,长度为 5 ,str3 也是数组类型,与str1都在同一个内存空间中,并且在 str1 之后声明 , 他们的地址应该是连续的;
  • 当改变 str1 的值为 “HelloWorld”时,str1之前的大小装不下这么多字符,就会使用后面的内存地址,str3 本是空的并没有赋值,但是由于 str1的内存溢出,装到了str3中,所以str3也是有值的;
  • 总之,数组内存溢出是件危险的事;

String类型输出遇到/0 结束
char类型输出遇到/0 继续输出.

指针变量char *str2 = "hello",用scanf 向str2中输入字符串出错,其实也可以这么理解,指针str2只是指向一个地址,从这个地址开始写入"hello",没有指定内存长度,没有空间去容纳字符串。内存溢出!这个与char str[] = "hello"不同,str已经有了6个字节的内存空间,

 1 #include <stdio.h>
 2 #include <string.h>
 3 
 4 int main(int argc, char *argv[])
 5 {
 6     char Queen[10];
 7 
 8     memset(Queen, 'G', sizeof(Queen));
 9 
10     return 0;
11 }

malloc函数和memset函数的操作语句一般如下:
int * p=NULL;
p=(int*)malloc(sizeof(int));
if(p==NULL)
    printf(“Can’t get memory!n”);
memset(p,0,siezeof(int));

 

(2)使用malloc函数分配的堆空间在程序结束之前必须释放

调用memset()函数之前:
(gdb) p Queen
$1 = "360\005@\000\000\000\000\000240\004"
调用memset()函数之后:
(gdb) p Queen
$2 = "GGGGGGGGGG"

从堆上获得的内存空间在程序结束以后,系统不会将其自动释放,需要程序员来自己管理。一个程序结束时,必须保证所有从堆上获得的内存空间已被安全释放,否 则,会导致内存泄露。我们可以使用free()函数来释放内存空间,但是,free函数只是释放指针指向的内容,而该指针仍然指向原来指向的地方,此时, 指针为野指针,如果此时操作该指针会导致不可预期的错误。安全做法是:在使用free函数释放指针指向的空间之后,将指针的值置为NULL。

 

(3)calloc函数的分配的内存也需要自行释放calloc函数的功能与malloc函数的功能相似,都是从堆分配内存,它与malloc函数的一个 显著不同时是,calloc函数得到的内存空间是经过初始化的,其内容全为0。calloc函数适合为数组申请空间,可以将size设置为数组元素的空间 长度,将n设置为数组的容量。

如果Queen数组类型不是char而是int,结果会怎样?

(4)如果要使用realloc函数分配的内存,必须使用memset函数对其内存初始化realloc函数的功能比malloc函数和calloc函数 的功能更为丰富,可以实现内存分配和内存释放的功能。realloc 可以对给定的指针所指的空间进行扩大或者缩小,无论是扩张或是缩小,原有内存的中内容将保持不变。当然,对于缩小,则被缩小的那一部分的内容会丢失。 realloc 并不保证调整后的内存空间和原来的内存空间保持同一内存地址。相反,realloc 返回的指针很可能指向一个新的地址。所以,在代码中,我们必须将realloc返回的值,重新赋值给 p :

 1 #include <stdio.h>
 2 #include <string.h>
 3 
 4 int main(int argc, char *argv[])
 5 {
 6     int Queen[10];
 7 
 8     memset(Queen, 'G', sizeof(Queen));
 9 
10     return 0;
11 }

p = (int *) realloc(p, sizeof(int) *15);

 

甚至,你可以传一个空指针(0)给 realloc ,则此时realloc 作用完全相当于malloc。

调用memset()函数之前:
(gdb) p Queen
$1 = {0, 0, 0, 0, 4195824, 0, 4195488, 0, -8352, 32767}
调用memset()函数之后:
(gdb) p Queen
$2 = {1195853639, 1195853639, 1195853639, 1195853639, 1195853639, 1195853639,

int* p = (int *)realloc (0,sizeof(int) * 10);   //分配一个全新的内存空间,

1195853639, 1195853639, 1195853639, 1195853639}

这一行,作用完全等同于:

看到了吧!Queen数组全部被填充了一个相同的数字,但该数字却不是我们想要的'G'。为什么会这样?道理很简单,memset()函数是一个字节一个字节地填充数字的,你一个int类型变量在内存中占了4个字节(不同机器,int所占字节数不一样),不出错才怪!上一个例子之所以能够正确运行,是因为在我的机器上,一个char类型变量刚好占了一个字节。

int* p = (int *)malloc(sizeof(int) * 10);

 

(5)关于alloca()函数
还有一个函数也值得一提,这就是alloca()。其调用序列与malloc相同,但是它是在当前函数的栈帧上分配存储空间,而不是在堆中。其优点是:当 函数返回时,自动释放它所使用的栈帧,所以不必再为释放空间而费心。其缺点是:某些系统在函数已被调用后不能增加栈帧长度,于是也就不能支持alloca 函数。尽管如此,很多软件包还是使用alloca函数,也有很多系统支持它。

怎么避免这个问题?下次记得在使用memset()函数对int型数组进行填充时,所能够使用的常量c的取值只能是0和-1。想想0和-1这两个数的二进制表示是怎样的,你就明白了为什么只能使用这两个数字了。

2、删除平衡二叉树中的一个节点;

 

3、static_cast 和 dynamaic_cast 的用法。

3.2 第二个坑
在成功避开第一个坑之后,你可能还会遇到第二个坑。比如说,你把上述的代码写成了下面的这个样子。

代码如下:

 1 #include <stdio.h>
 2 #include <string.h>
 3 
 4 int main(int argc, char *argv[])
 5 {
 6     int Queen[10];
 7 
 8     memset(Queen, -1, 10);
 9 
10     return 0;
11 }
 1 #include <iostream>
 2 
 3 using namespace std;
 4 
 5 
 6 class Base
 7 {
 8 public:
 9     Base(){}
10     ~Base(){}
11     virtual void Output(int i)
12     {
13         cout<<"Base::Output value is "<<i<<endl;
14     }
15 };
16 
17 class Derived1:public  Base
18 {
19 public:
20     Derived1(){}
21     ~Derived1(){}
22 
23     virtual void Output(int i)
24     {
25         cout<<"Derived1::Output value is "<<i<<endl;
26     }
27     
28     void Output2()
29     {
30         cout<<"Dervied1:Output2"<<endl;
31     }
32 
33 };
34 
35 class Dervied2:public Base
36 {
37 public:
38     Dervied2(){}
39     ~Dervied2(){}
40     virtual void Output(int i)
41     {
42         cout<<"Dervied2::Output value is "<<i<<endl;
43     }
44 
45     void Output2()
46     {
47         cout<<"Dervied2:Output2"<<endl;
48     }
49 };
50 
51 
52 
53 int main()
54 {
55     Base *p= new  Dervied2;
56     Derived1 *p1= static_cast<Derived1*>(p);
57     if(p1)
58     {
59         p1->Output(1);
60         p1->Output2();
61     }
62     cout<<"=========================n";
63 
64     p1= dynamic_cast<Derived1*>(p);
65     if(p1)
66     {
67         p1->Output(2);
68         p1->Output2();
69     }
70 
71     system("pause");
72 }

 

转自:

调用memset()函数之前:
(gdb) p Queen
$1 = {0, 0, 0, 0, 4195824, 0, 4195488, 0, -8352, 32767}
调用memset()函数之后:
(gdb) p Queen
$2 = {-1, -1, 65535, 0, 4195824, 0, 4195488, 0, -8352, 32767}

 

怎么回事,为什么没有把Queen数组全部初始化为-1?因为memset()函数是一个字节一个字节地填充数的!第三个参数10表示的是数组Queen中有10个int型数据,它并不表示Queent数组在内存中所占的字节数!明白了吧!

 

怎么避免这个问题,很简单,将上述memset()调用改成如下的样子就好了

1 memset(Queen, -1, sizeof(Queen));

 

四 参考资料

  1. man memset

本文由星彩网app下载发布于计算机编程,转载请注明出处:C语言指针与内存,函数中的坑

TAG标签: 星彩网app下载
Ctrl+D 将本页面保存为书签,全面了解最新资讯,方便快捷。