C++动态内存分配和数组及举例

***接上一篇博客:C++指针***

十九、动态分配内存

19.1 C++内存模型

C++的内存模型可以简单描述为包括四个主要的区域:栈区、堆区、全局区(数据段)和程序代码区(代码段)

1.栈区(Stack)栈区是由编译器自动管理的内存区域用于存储函数调用的上下文信息局部变量函数参数等。栈区的分配和释放速度非常快,遵循先进后出(LIFO)原则。栈区的大小是有限的,由编译器设置,默认情况下栈区大小在几MB到几GB之间。

2.堆区(Heap)堆区是由开发人员手动分配释放的内存区域用于存储动态分配的变量对象。堆区的分配和释放需要使用特定的操作符,如`new``delete`。堆区的大小比较大,一般要比栈区大得多。在堆区中,变量的生命周期不受函数的限制,即可以在函数外部使用。 3.全局区(Data Segment): 全局区用于存储全局变量静态变量常量等,在程序运行期间始终存在。全局区的变量可以在程序的任何位置访问。全局变量和静态变量都保存在全局区中,静态变量的生命周期延长到整个程序的运行期间。

4.程序代码区(Code Segment): 程序代码区存放着程序的执行代码,包括所有的函数和操作指令。程序代码区是只读的,不允许对其进行写操作。 此外,还有一些其他的内存区域,如常量区、字符串字面量区等,它们用于存储常量和字符串字面量等。 需要注意的是,不同的编译器和操作系统对内存模型的实现可能存在细微的差异,上述描述仅为一般情况下的内存模型概述。在实际编程中,需要根据具体的编译器和操作系统的规范来合理地使用内存。

栈和堆的主要区别:

1)管理方式不同: 栈是编译器自动管理的,在出作用域时,将自动被释放,堆需手动释放,若程序中不释放,程序结束时由操作系统回收。

2)空间大小不同: 堆内存的大小受限于物理内存空间,而栈就小得可怜,一般只有 8M(可以修改系统参数)。

3)分配方式不同: 堆是动态分配;有静态分配和动态分配(都是自动释放)。

4)分配效率不同: 栈是系统提供的数据结构,计算机在底层提供了对栈的支持,进栈和出栈有专门的指令,效率比较高;堆是由 C++函数库提供的。

5)是否产生碎片: 进栈和出栈都有着严格的顺序(先进后出),不会产生碎片,而堆频繁的分配和释放,会造成内存空间的不连续,容易产生碎片,太多的碎片会导致性能的下降。

6)增长方向不同: 栈向下增长,以降序分配内存地址,堆向上增长,以升序分配内存地址。

19.2 动态分配内存new和 delete

使用堆区内存的有四个步聚:

1)声明一个指针;

2)用new运算符向系统申请一块内存,让指针指向这块内存;

3)通过对指针解引用的方法,像使用变量一样使用这块内存;

4)如果这块内存不用了,用 delete 运算符释放它;

申请内存的语法:new 数据类型(初始值);// C++11 支持{}

如果申请成功,返回一个地址,如果申请失败,返回一个空地址(暂时不考虑失败的情况)。

释放内存的语法: delete 地址

注意:

·动态分配出来的内存,没有变量名,只能通过指向它的指针来操作内存中的数据。

·如果动态分配的内存不用了,必须用 delete 释放它,否则有可能用尽系统的内存

·动态分配的内存生命周期与程序相同,程序退出时,如果没有释放,系统将自动回收。

·就算指针的作用域已失效,所指向的内存也不会释放。

·用指针跟踪已分配的内存时,不能跟丢。

二十、数组

数组是一组数据类型相同的变量,可以存放一组数据。

20.1 一维数组

1)创建数组

声明数组的语法:数据类型 数组名[数组长度]

注意:数组长度必须是整数,可以是常量,也可以是变量和表达式。

C90规定必须用常量表达式指明数组的大小,C99允许使用整型非常量表达式。经测试,在VS中可以用整型非常量表达式,不能用变量,但是,Linux 中还可以用变量。

2)数组的使用

可以通过下标访问数组中元素,数组下标从0开始。

数组中每个元素的特征和使用方法与单个变量完全相同。

语法:数组名 [数组下标]

注意:

·数组下标也必须是整数,可以是常量,也可以是变量。

·合法的数组下标取值是: 0~(数组长度-1)

3)数组占用内存的情况

数组在内存中占用的空间是连续的。

用sizeof(数组名)可以得到整个数组占用内存空间的大小(只适用于C++基本数据类型)

4)数组的初始化

声明的时候初始化:

数据类型 数组名[数组长度] =[ 值 1,值 2 ...}

数据类型 组名[] =[ 值 1,值 2 ...}:

数据类型 数组名[数组长度] = { 0 }; // 把全部的元素初始化为 0。

注意:

如 {} 内不足数组长度个数据,剩余数据用0补全,但是,不建议这么用,你可能在数组中漏了某个值。如果想把数组中全部的元素初始化为0,可以在0内只填一个0或什么也不填。

5)清空数组

memset()函数可以把数组中全部的元素清零。 (只适用于C++基本数据类型)

函数原型:void * memset ( void * ptr, int value, size_t num );(num是字节数)

注意,使用memcpy()函数需要包含头文件#include <string.h>

例:

#include<iostream>

#include<string>

using namespace std;

int main()

{

int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };

memset(arr, 0, 40);//重置数组的各个元素

for (int i = 0; i < 10; i++)

{

cout << arr[i]<<"\n";

}

return 0;

}

6)复制数组

用 memcpy{}函数可以把数组中全部的元素复制到另一个相同大小的数组。 (只适用于 C++基本数据类型)

函数原型:void * memcpy ( void * destination, const void * source, size_t num );

#include<iostream>

#include<string.h>

int main()

{

int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };//原始数组

int a[10] = { 0 };//

memcpy(a, arr, 100);

for (int i = 0; i < 10; i++)

{

std::cout << a[i]<<"\n";

}

return 0;

}

20.2 一维数组和指针

1)指针的算术

将一个整型变量加1后,其值将增加 1。

但是,将指针变量(地址的值)加1后,增加的量等于它指向的数据类型的字节数。

2)数组的地址

a)数组在内存中占用的空间是连续的;

b) C++将数组名解释为数组第0个元素的地址;

c)数组第0个元素的地址和数组的首地址取值是相同的;

d)数组第n个元素的地址是:数组首地址+n

e) C++编译器把 数组名[下标] 解释为 *(数组首地址+下标)

3)数组的本质

数组是占用连续空间的一块内存,数组名被解释为数组第0个元素的地址。

C++操作这块内存有两种方法:数组解释法和指针表示法,它们是等价的。

4)数组名不一定会被解释为地址

在多数情况下,C++将数组名解释为数组的第0个元素的地址,但是,将 sizeof 运算符用于数组名时,将返回整个数组占用内存空间的字节数。

可以修改指针的值,但数组名是常量,不可修改

注意:

数组名实际上是一个指向数组第一个元素的指针。因此,当你尝试打印数组的地址时,你会得到一个指向整数的指针,而不是一个长整数。这就是为什么你需要将结果强制转换为long long类型,以便正确地显示64位地址

如果你不使用long long,你可能只会看到32位地址的一部分,这可能会导致错误的结果。例如,如果你的系统是64位的,那么32位地址可能会被解释为无效地址,导致程序崩溃

20.3 一维数组用于函数的参数

1)指针的数组表示

在C++内部,用指针来处理数组。企

C++编译器把 数组名[下标] 解释为 *(数组首地址+下标);

C++编译器把 地址[下标] 解释为 *(地址+下标);

2)一维数组用于函数的参数

维数组用于函数的参数时,只能传数组的地址,并且必须把数组长度也传进去,除非数组中有最后一个元素的标志。

书写方法有两种:

void func(int* arr, int len):

void func(int arrll, int len);

注意:

在函数中,可以用数组表示法,也可以用指针表示法。

在函数中,不要对指针名用 sizeof 运算符,它不是数组名。

20.4 用new动态创建一维数组

普通数组在栈上分配内存,栈很小;如果需要存放更多的元素,必须在堆上分配内存

动态创建一维数组的语法:数据类型* 指针=new 数据类型[数组长度];

释放一维数组的语法:delete[] 指针;

注意:

·动态创建的数组没有数组名,不能用 sizeof 运算符

·可以用数组表示法指针表示法两种方式使用动态创建的数组。

·必须使用 delete[]来释放内存。(不能只用 delete)

·不要用 delete[]来释放不是 newl分配的内存。

·不要用 deletel]释放同一个内存块两次 (否则等同于操作野指针)。

·对空指针用 delete[]是安全的 (释放内存后,应该把指针置空 nullptr)。

·声明普通数组的时候,数组长度可以用变量,相当于在栈上动态创建数组,并且不需要释放

·如果内存不足,调用new 会产生异常,导致程序中止;如果在 new 关键字后面加(std::nothrow)选项,则返回 nullptr,不会产生异常。(#include <cstdlib>)

·为什么用 delete口释放数组的时候,不需要指定数组的大小? 因为系统会自动跟踪已分配的内存。

举例:

#include<iostream>

#include<cstdlib>

int main()

{

int* p = new(std::nothrow)int[10];

for (int i = 0; i < 10; i++)

{

p[i] = i;

}

for (int i = 0; i < 10; i++)

{

std::cout<<"p["<<i<<"]==" << p[i] << std::endl;

}

delete[] p;

return 0;

}



20.5 数组的排序 qsort

qsort0函数用于对各种数据类型的数组进行排序。

函数的原型:

void qsort(void *base, size t nmem, size t size, int (*compare)(const void *, const void *))

第一个参数:数组的起始地址。

第二个参数:数组元素的个数(数组长度)。

第三个参数:数组元素的大小 (sizeof(数组的数据类型))。

第四个参数:回调函数的地址。

回调函数决定了排序的顺序,声明如下:

int compare(const void *p1, const void *p2):

1)如果函数的返回值< 0,那么 p1所指向元素会被排在 p2所指元素的前面;

2)如果函数的返回值==0,那么 p1 所指向元素与 p2所指元素的顺序不确定;

3)如果函数的返回值> 0,那么 p1所指元素会被排在 p2所指元素的后面.

在 sort 函数中,我们可能会对数组进行排序,这可能涉及到元素的移动和交换。如果我们不保护这些元素不被修改,那么在排序过程中可能会出现错误。因此,我们需要使用 const 关键字来确保这些元素不会被修改

qsort0函数的其它细节:

·形参中的地址用 void 是为了支持任意数据类型,在回调函数中必须具体化。

·size_t是C标准库中定义的,在64 位系统中是8字节无符号整型(unsigned long long)。

·排序的需求除了升序和降序,还有很多不可预知的情况,只能用回调函数。

#include<iostream>

#include<cstdlib>

int compare(const void* a, const void* b)

{

return *((int*)a) - *((int*)b);

}

int main()

{

int a[10] = { 1,2,5,7,8,9,4,3,8,3 };

std::cout << "排序前:" ;

for (int i = 0; i < 10; i++)

{

std::cout << a[i] << " ";

}

std::cout << "\n";

qsort(a, sizeof(a) / sizeof(int), sizeof(int), compare);

std::cout << "排序后:" ;

for (int i = 0; i < 10; i++)

{

std::cout << a[i] << " ";

}

return 0;

}

20.6 折半查找(二分查找)

20.7 C风格的字符串

C语言约定:如果字符型(char)数组的末尾包含了空字符0(也就是0),那么该数组中的内容就是一个字符串.

因为字符串需要用0结尾,所以在声明字符数组的时候,要预留多一个字节用来存放0。

char name[21]; // 声明一个最多存放 20 个英文字符或十个中文的字符串。

1)初始化方法

char name[11];//可以存放 10 个字符,没有初始化,里面是垃圾值。

char name[11] = "hello";// 初始内容为 hello。

char namel]= [ "hello" ];// 初始内容为 hello系统会自动添加\0,数组长度是 6。

char name[11] = ( "hello" );// 初始内容为 hello.

char name[11] "hello"; // 初始内容为 hello。

Char name[11]={0};// 把全部的元素初始化为\0。

2)清空字符串

memset(name,0,sizeof(name));// 把全部的元素置为 0。

name[0]=0://不规范,有隐患,不推荐

3)字符串复制或赋值 strcpy()

char *strcpy(char* dest, const char* src);

例:strcpy (str2,str1);

功 能:将参数src字符串拷贝至参数 dest 所指的地址。

返回值: 返回参数 dest 的字符串起始地址。

头文件:#include<cstring>

复制完字符串后,会在 dest 后追加 0。

如果参数 dest 所指的内存空间不够大,会导致数组的越界。

4)字符串复制或赋值 strncpy

char * strncpy(char* dest,const char* src, const size t n);

功能:把src前n个字符的内容复制到 dest 中

返回值: dest 字符串起始地址

如果src 的长度大于等于n,就截取 src 的前 n个字符,不会在 dest 后追加0。

如果参数 dest 所指的内存空间不够大,会导致数组的越界

5)获取字符串的长度strlen()

Size_t strlen( const char*str);

功能:计算字符串的有效长度,不包含 0。

返回值: 返回字符串的字符数。

strlen0函数计算的是字符串的实际长度,遇到0结束。

6)字符串拼接 strcat()

char *strcat(char* dest,const char* src);

功能:将src 字符串拼接到 dest 所指的字符串尾部。

返回值: 返回 dest 字符串起始地址。

dest 最后原有的结尾字符0会被覆盖掉,并在连接后的字符串的尾部再增加一个 0。

如果参数 dest 所指的内存空间不够大,会导致数组的越界。

7)字符串拼接 strncat()

char *strncat (char* dest,const char* src, const size_t n);

功能:将src字符串的前n个字符拼接到 dest 所指的字符串尾部。“

返回值: 返回 dest 字符串的起始地址。“

如果n大于等于字符串 src的长度,那么将 src全部追加到dest 的尾部,如果n小于字符串 src的长度,只追加 src的前 n 个字符。e

strncat 会将 dest 字符串最后的0覆盖掉,字符追加完成后,再追加0。4

如果参数 dest 所指的内存空间不够大,会导致数组的越界。

8)字符串比较 strcmp0)和 strncmp(

nt strcmp(const char *str1, const char *str2 )

功能:比较str1和str2的大小。“

返回值:相等返回0,str1 大于 str2返回1,str1小于str2 返回-1;

nt strncmp(const char *str1,const char *str2 ,const size t n)

功能:比较str1和str2前n个字符的大小。

返回值:相等返回0,str1 大于 str2 返回1,str1小于str2 返回-1;“

两个字符串比较的方法是比较字符的 ASCII 码的大小,从两个字符串的第一个字符开始,如果分不

9)字符查找 strchr()和strrchr()

har *strchr(const char *s,const int c);

返回一个指向在字符串s 中第一个出现c的位置,如果找不到,返回0。个

10)字符串查找 strstr()4

har *strstr(const char* str,const char* substr);

功能:检索子串在字符串中首次出现的位置。“

返回值: 返回字符串 str 中第一次出现子串 substr 的地址,如果没有检索到子串,则返回0。

11)用于string的表达式

可以把C风格的字符串用于包含了 string 类型的赋值拼接等表达式中。

12)注意事项(

a)字符串的结尾标志是0,按照约定,在处理字符串的时候,会从起始位置开始搜索 0,一直找下去,找到为止(不会判断数组是否越界)。“

b)结尾标志0后面的都是垃圾内容。“

c)字符串在每次使用前都要初化,减少入坑的可能,是每次,不是第一次。

不要在子函数中对字符指针用 sizeof 运算,所以,不能在子函数中对传入的字符串进行初始化,除非字符串的长度也作为参数传入到了子函数中。“

e)在VS中,如果要使用C标准的字符串操作函数,要在源代码文件的最上面加

#define _CRT_SECURE_NO_WARNINGS

20.8 二维数组

一维数组的数学概念是线性表,二维数组的数学概念是矩阵。

1)创建二维数组

声明二维数组的语法:数据类型 数组名[行数][列数];

注意:数组长度必须是整数,可以是常量,也可以是变量和表达式

C90规定必须用常量表达式指明数组的大小,C99允许使用整型非常量表达式。经测试,在VS中可以用整型非常量表达式,不能用变量;但是,Linux 中还可以用变量。

2)二维数组的使用

可以通过行下标和列下标访问二维数组中元素,下标从0开始。

二维数组中每个元素的特征和使用方法与单个变量完全相同。

语法:数组名[行下标][列下标];

注意:

·二维数组下标也必须是整数,可以是常量,也可以是变量。

·合法的行下标取值是:0~(行数-1)。

·合法的例下标取值是:0~(列数-1)。

3)二维数组占用内存的情况

sizeof(数组名)可以得到整个二维数组占用内存空间的大小(只适用于 C++基本数据类型)。

二维数组在内存中占用的空间是连续的

4)二维数组的初始化

声明的时候初始化:

数据类型 数组名[行数][列数] = {{据 1,数据 2},{据 3,数据 4 },..... .};

数据类型 数组名[行数][列数] ={ 数据 1,数据 2,数据 3,数据 4,.....};

......

C++11 标准可以不写等于号。

  1. 清空二维数组

memset()函数可以把二维数组中全部的元素清零函数原型:

void *memset(void *s, int c, size_t n)

注意,在 Linux 下,使用 memset()函数需要包含头文件#include <string.h>

  1.  复制二维数组

用memcpy0函数可以把二维数组中全部的元素复制到另一个相同大小的数组(没说多少维)。

(只适用于 C++基本数据类型)

函数原型:void *memcpy(void *dest, const void *src, size_t n);

注意,在 Linux下,使用 memcpy0函数需要包含头文件#include <string.h>

20.9 二维数组用于函数的参数

int* p;//整型指针。

int* p[3];//一维整型指针数组,元素是 3 个整型指针 (p[]、p[1]、p[2])

int* p();// 函数 p 的返回值类型是整型的地址

int (*p)(int ,int);// p 是函数指针,函数的返回值是整型。

1)数组指针

声明数组指针的语法:数据类型 (*行指针名)[行的大小]; // 行的大小即数组长度

例;

int(*p1)[3]; // p1 是数组指针,用于指向数组长度为 3 的 int 型数组。

double (*p3)[5]; // p2 是数组指针,用于指向数组长度为 5 的 double 型数组。

一维数组名被解释为数组第0个元素的地址。

2)把二维数组传递给函数

int bh[2][3] = ( (11,12,13)(21,22,23) );

int (*p)[3]=bh;

如果要把 bh 传给函数,函数的声明如下:

func(int (*p)[3],int len):

void func(int pll[3],int len);


本章完。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

万叶学编程

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值