内存管理的数据结构,数据结构主要研究内存中数据组织和数据处理方法
第一条指针1.1概念1.2空指针1.3指向函数的指针1.4.1原理1.5指针算法2数组2.1数组长度类型2.2数组名称2.3索引2.4初始化3字符串的字面量3.1初始化字符串指针数组4主函数的参数5文件读写5.1标准错误流5.2输入/输出文件6三种内存管理方式6.1自动内存管理6.2为对象分配/释放内存6.3为数组分配/释放内存
理解标准库的关键是使用核心语言编程工具的技巧(“低级”)。它们比标准库更低,更接近通用计算硬件的工作模式。因此,它比标准库更难使用,也更危险。
因为标准库不能解决所有问题,所以C程序中经常使用“低级”技能。
指针c的变量其实是一个字节,比如char是1个字节,32位int是4个字节,64位long long是8个字节。为了正确访问这些字节,每个变量字节都有一个唯一的编号。我们称内存中的字节数为地址或指针。存储指针的变量叫做指针变量,比如int*。
例如
int x=10
//假设X的地址是0x7ff7bdf8b888
int * p=x;
//那么P存储的值就是0x7ff7bdf8b888
* p;//是对P的值(即地址)取实际存储的值10
1.1概念指针存储对象地址的值。
假设X是一个对象,那么X是对象的地址;
如果p是对象的地址,那么*p就是对象本身。
其中是地址运算符,而*p中的*是间接引用运算符。
对象可以理解为只包含对象一个元素的“容器”,指向对象的指针可以理解为指向“容器”中唯一元素的迭代器。
1.2空指针指针类型的局部变量在赋值之前没有任何有意义的值。
通常使用0作为初始指针变量,因为将0转换为指针值可以确保生成一个不同于指向特定对象的指针的值。
常数0也是唯一可以转换为指针类型的整数值。0转换成的指针类型值为空指针。
1.3用于定义指针变量
//定义一个int指针变量
int * p;
int * p;//C程序员习惯用法容易出错:
在下面的文字中,实际上定义了int*类型的指针P和int类型的Q变量。
int* p,q;示例程序:
#包括
使用STD:cout;
使用STD:CIN;
使用STD:endl;
int main(int argc,char** argv){
int x=5;
int * p=x;//指向x地址的指针
cout x= x endl
* p=6;
cout x= x endl
返回0;
}在上面的程序中,一旦P存储了X的地址,*p和X将是指向同一个对象的两个完全等价的方法。
1.4指向函数的指针
1.4.1使用下面的代码,*p的类型是int,p是一个指针。
int * p;在下面的代码中,我们间接引用fp,用int变量作为参数调用它,返回的结果也是int类型。也就是说,fp是一个指向函数的指针,这个函数有一个int类型的参数并返回一个int类型的结果。
int(* FP)(int);
1.4.2主函数不是对象,不能复制或赋值,也不能直接作为参数使用。尤其是在程序中创建或修改函数是不可能的(只有编译器可以)。
一个程序对一个函数所做的就是调用这个函数或者获取它的地址。
如果函数名出现在任何地方,而不是调用函数,编译器将把它解释为函数的地址,即使没有显示用法语句。
如果我们有一个函数匹配fp函数类型
int next(int n){
返回n ^ 1;
}那么下面任何一种写法都是等价的。
int(* FP)(int);
fp=next//相当于以下内容
fp=next如果有一个int变量I,通过fp函数调用next可以使I增加1。以下两种实现方法是等效的:
int I;
I=(* FP)(I);//相当于以下内容
I=FP(I);完整的示例程序:
#包括
使用STD:cout;
使用STD:CIN;
使用STD:endl;
int next(int n){
返回n ^ 1;
}
int main(int argc,char** argv){
int(* FP)(int);
fp=next//将fp指向下一个函数
//FP=next;
int I=(* FP)(6);
//int I=FP(6);
cout i endl
返回0;
}如果你写了一个表面上把另一个函数作为参数的函数,编译器会在背后悄悄地把参数转换成指向这个函数的指针。
例如:
以下两种写法是等价的:
void write(ostream,int test(int),int)
Voidwrite 2 (ostream,int (* test) (int),int)完整示例程序:
//返回一个指向函数的指针
#包括
#包括
使用STD:cout;
使用STD:CIN;
使用STD:endl;
使用STD:ostream;
int test1(int a){
返回1;
}
int test2(int b){
返回B3;
}
void write(ostream out,int test(int),int a){
out test(a)endl;
}
void write2(ostream out,int (*test)(int),int a){
out test(a)endl;
}
int main(int argc,char** argv){
int x=10
write(cout,test1,x);
write(cout,test2,x);
write2(cout,test1,x);
write2(cout,test2,x);
返回0;
}但是对于函数的返回值,不会自动执行这种转换。如果我们要写一个函数,它的返回类型是指向该函数的指针,并且要求它的返回类型与write2函数的返回类型相同,那么我们必须显示返回指针的声明。
以下两种显示方法是相同的:
typedef int(* test _ FP)(int);
fp测试();等于
int(* test())(int);如果调用test()函数作为结果,并间接引用结果,那么将得到一个返回类型为int,参数类型为int的函数。
两个声明方法,返回一个指向带有int引用变量参数的函数的指针:
//声明
typedef int(* test _ FP)(int);
test _ FP test3(int);相当于以下内容
//声明
int(* test 4(int))(int);完整代码:
//返回一个指向函数的指针
#包括
#包括
使用STD:cout;
使用STD:CIN;
使用STD:endl;
使用STD:ostream;
int test1(int a){
a;
返回a;
}
//声明
typedef int(* test _ FP)(int);
test _ FP test3(int);
//定义
test_fp test3(int x){
int(* FP)(int)=test1;
(* FP)(x);//x的值变成11
返回FP;
}
//声明
int(* test 4(int))(int);
//定义
int (*test4(int x))(int ){
int(* FP)(int)=test1;
(* FP)(x);//x的值变成11
返回FP;
}
void write2(ostream out,int test(int),int a){
out test(a)endl;
}
int main(int argc,char** argv){
int x=10
//当//测试通过时,引用会导致x的变化,当write通过时,没有引用,计算完值后丢弃。
//10加2
//第一次调用test时加1。因为是引用,所以X实际上会变成11。第二次调用write时加1。虽然结果是12,但是X的值仍然是11。
write2(cout,test3(x),x);
write2(cout,test4(x),x);//11加2
返回0;
}指向库算法中函数的指针通常用于另一个函数的参数。
中的模板类,Pred类
In find_if(In begin,In end,Pred f){
while(开始!=结束!f(*begin)){
开始;
}
返回开始;
}当f(*begin)有一个有意义的值时,Pred可以是任何类型。假设一个判断函数,定义如下:
bool is_negative(int n){
返回n 0;
}我们用find_if寻找向量v中的第一个负元素。
vector int:iterator I=find _ if(v . begin(),v.end(),is _ negative);我们可以把is_negative写成is_negative,编译器会自动把函数名转换成函数的指针。
也可以在find_if函数的实现代码中把(*f)(*begin)写成f(begin),编译器会自动把对函数指针的调用解释为调用指针所指向的函数。
1.5指针算法指针是一个随机内存迭代器,所以有
随机迭代器的必要条件(p和q是迭代器,n是整数):
P,p-n,n pp-q两个迭代器相减(p-q)会产生一个整数,表示容器中P和Q指向的元素之间的距离。P-q是有符号整数类型,因为它可能是负数。类型是整数(int)还是长整型取决于系统环境。在标准库中,cstddef中提供了ptrdiff_t来表示这样的类型。
除了指针加法,还可以做指针减法,指针可以和整数加减。
对于数组
双a[3];
a是一个有效的指针,尽管它不指向数组a中的任何元素。与容器string和vector类似,将N添加到只有N个元素的数组的第一个地址会得到一个新地址,该地址不指向数组中的任何对象,但它是有效的。
对于指针p和整数n相关的p,p n,p-n,即使有些可能超出数组的地址范围,但都是有效的,只是不可预测而已。
不允许计算指向容器前面地址的迭代器。在数组前面计算地址被认为是非法的。
ArrayArray是一种容器,类似于vector,但没有vector的强大功能(不能动态增减大小)。
指针和数组是C/C中最原始的数据结构之一,只使用数组而不使用指针是不可能解决任何问题的。指针的强大功能在使用数组时可以得到更好的体现。
数组不是类,所以没有成员函数或成员变量。
2.1数组长度类型cstddef头文件中的size_t类型(无符号整数)常用来表示数组的大小,size_t类型的大小可以安装在任何对象中。
2.2数组名只要我们使用数组名作为值,数组名将代表一个指向数组第一个地址的指针。也就是阵列?第一个地址被保存。
您可以使用*运算符间接引用数组来访问该指针所指向的对象。
例如:
双a[3];
* a=6.6//将6.6赋给数组a的第一个元素。
2.3 Index和所有随机访问迭代器一样,它们(指针)都支持Index函数。
如果p指向数组中的第m个元素,那么p[n]代表数组本身的第n个元素。(不是元素的地址,而是元素本身)。
如果a是一个数组,那么a[n]是该数组的第n个元素。或者,如果p是指针,n是整数,那么p[n]和*(p n)是等价的。
2.4初始化我们可以避免显示的定义数组的大小,
诸如
int a[]={1,2,3,4,5,6 };取数组1为数字0,6为数字5。
编译器会自动计算数组的数量。
3字符串文字字符串文字:字符文字的数组,其大小为字符串的长度加1。额外的字符是一个空字符( \0 ),由编译器自动添加在其他字符之后。
换句话说
const char love[]={L , o , v , \ 0 };love数组相当于字符串文字“Love”。
注意:当一个字符数组被定义为一个静态变量时,它会自动在数组的末尾加上 \0 。
static char love[]={L , o , v , e };//初始化时,在末尾自动添加 \0strlen函数,可以找到字符串变量的大小或者以空字符结尾的数组的大小。strlen函数返回的大小不包括终止符,即空字符。
strlen的库函数实现;
size_t strlen(const char* p){
size _ t size=0;
while(*p 1=\0){
尺寸;
}
返回大小;
}其中size_t是一个无符号整数类型,大到足以容纳任何数组。
由于爱数组等价于字符串文字“爱”,所以有
弦s(爱);
字符串S2(‘爱’);
string s3(love,love strlen(爱));以上三者是等价的。
3.1初始化字符串指针数组静态常数double numbers[]={
97, 94, 90, 87, 84, 80, 77, 74, 70, 60, 0
};
static const size _ t n grades=sizeof(数字)/sizeof(*数字);//计算这个元素的数目
//整个数组的大小/每个元素所占的空间。在上面的程序中,static用于告诉编译器在使用numbers数组之前只初始化它一次。如果不使用static,编译器会在每次调用前初始化数组。很明显,这会减慢程序的速度。
sizeof运算符返回的值以字节为单位,这是实际的存储单位,并随不同的编程工具而变化。
字节唯一确定的是,一个字节包含8位,每个对象至少占用一个字节,一个字符(char)变量正好占用一个字节。
注意:
返回?\?\?;//C不允许两个或多个连续的问号。
4主函数的参数主函数有两个参数,一个整数(int)参数(argc)和一个指向字符指针的指针参数(argv)。
Argv是指向指针数组第一个元素地址的指针。数组中的每个元素都指向一个字符串参数。
c是argc指向的数组中指针的数量。
argv数组的第一个元素总是main函数编译的程序名,所以argc的值至少为1。如果有参数,这些参数总是作为数组中的几个连续元素出现。
例如,在下面的程序中,执行。cpp文件并添加参数Hello,word,程序就会输出。
第一个参数是文件名。
//主函数的参数
#包括
使用STD:cout;
使用STD:CIN;
使用STD:endl;
int main(int argc,char** argv){
if(argc 1){
cout argv[0];
for(int I=1;我!=argci) {
cout argv[I];
}
}
cout endl
cout argc endl
返回0;
}一般情况下,char* argv[]和char**是等价的,但是语法char* argv[]只有出现在参数列表中才是合法的。
5文件读写
5.1标准错误流程如果程序出现异常,我们希望有一个输出异常的方法,会告诉用户程序出现异常,或者可以在异常发生后建立一个事件日志。
Clog stream倾向于生成日志,所以clog stream和cout具有相同的缓冲特性;平时存储错误信息,在系统认为合适的时候输出。
Cerr流程是即使输出错误信息,也能保证在异常发生时,错误信息会及时显示出来。
5.2输入/输出文件文件的输入/输出对象和用于流的输入/输出对象是不同的类型。
在标准库中,ifstream类型对象被定义为一种istream,ofstream是一种ostream。所以只要需要istream的地方就可以用ifstream,只要需要iStream的地方就可以用ofstream。
当定义ifstream或ofstream类型的对象时,会要求您提供一个指针,指向空字符末尾的字符数组的第一个元素,例如string类型的文件名。
原因是:
1这样,程序可以使用输入/输出库的特性,而不依赖于字符串类型的特性;2/输出库比字符串类早几年出现;3以这种方式提供文件名时,可以使程序更容易与操作系统的输入/输出功能建立接口。一般来说都是通过指针来交流的。如果不想将文件名定义为字符串文字,可以将文件名存储在string类型的变量中,然后使用c_str成员函数。示例程序如下:
//处理文件的读写
#包括
#包括
#包括
使用STD:cout;
使用STD:CIN;
使用STD:endl;
使用STD:if stream;
使用STD:of stream;
使用STD:string;
int main(int argc,char** argv){
string file=/Users/macbook pro/clion projects/ACM/infile . txt ;
ifstream infile(file . c _ str());
//ifstream infile(/Users/macbook pro/clion projects/ACM/infile . txt );//相当于上面两行
of stream outfile(/Users/macbook pro/clion projects/ACM/outfile . txt );
字符串s;
while(getline(infile,s)){
outfile s endl
}
返回0;
}从主函数参数中复制一个或多个文件:
//主函数的参数
#包括
使用STD:cout;
使用STD:CIN;
使用STD:endl;
使用STD:cerr;
#包括
使用STD:if stream;
#包括
使用STD:string;
int main(int argc,char** argv){
int fail _ count=0;
for(int I=1;i argci) {
if stream in(argv[I]);
If(in){//如果文件存在,复制文件的内容,
字符串s;
while(getline(in,s)){
cout s endl
}
}else{//否则,将生成一条错误消息
cerr 无法打开文件 argv[I]endl;
失败计数;
}
}
返回fail _ count
}
6内存管理的三种方式
6.1自动内存管理这种方法通常与局部变量相关联。
一个局部变量只有在程序执行到该变量的定义时才被系统自动分配内存,当包含该变量定义的模块结束时,该变量所占用的内存被自动释放。
下面的程序中,函数返回一个局部变量的地址,但是当函数返回时,定义的局部变量X的语句块也被终止,X占用的内存被释放。因此,X创建的指针现在是无效的,并将返回一个意外的值。
int* invalid_pointer(){
int x;
返回x;//灾难
}你可以声明一个静态变量X来解决刚才的问题。系统变量X的内存只分配一次,然后这个变量占用的内存直到程序结束才会释放。
int* pointer_to_static(){
静态int x;
返回x;//合法
}但是静态分配内存有一个缺点,每次调用指向静态变量的指针都会返回指向同一个对象的指针。
如果你想定义一个函数,每次调用这个函数都会返回一个指向特定新对象的指针,这个对象会一直存在,直到我们不再需要它。为了实现这个目标,可以使用动态内存分配。
6.2为对象分配/释放内存。如果T是一个对象的类型,新的T表达式将为T类型的对象分配内存,该对象由构造函数初始化,并生成一个指向新分配的内存对象(未命名)的指针。
执行初始化语句,比如new T(args ),可以为变量指定一个特定的值。
新创建的对象将一直存在,直到程序结束或delete p语句被执行。(p是新语句中返回的指针)
为了删除指针,指针必须指向用新语句分配内存的对象,或者零指针。什么都不做就删除一个零点指针。
//示例程序
int * p=new int(42);
* p;//p现在等于43
删除p;//删除new创建的对象,但是指针还没有删除,现在指向一个不可预知的值。
6.3为数组分配/释放内存如果T为类型名,n为非负整数,那么new T[n]语句将为一个包含n个T类型对象的数组分配内存,并返回一个指向数组第一个元素的指针(指针类型为T*)。
默认情况下,每个对象都将被初始化,也就是说,如果T是内置类型,并且数组在局部范围内分配内存,则该对象不会被初始化。如果T是一个类,数组中的每个元素都将通过运行该类的默认构造函数来初始化。
如果T是自定义类型,请注意:
1如果这个类不允许缺省初始化,那么编译器将终止程序;数组中n个元素的每一个都将被初始化,这会带来一些运行开销。当使用new T[n]为数组分配内存时,如果n的值为零,则该子数组将不包含任何元素。
发生这种情况时,新函数无法返回指向第一个元素的指针(数组中根本没有元素)。其实新函数此时会返回一个有效但无意义的off-the-end指针,可以作为delete[]的参数。
在下面的示例程序中,当n等于0时,它仍然可以执行。
int n=0;
int * p=new int[n];//将返回一个有效但无意义的结束指针
//将其视为指向第一个元素的指针(如果存在的话)
向量int v(p,p n);
删除[]p;为了使用delete[],这里的[]括号是必不可少的。它告诉系统释放整个数组占用的内存,而不仅仅是一个元素的内存。
一旦一个元素用new[]分配了内存,这个内存就会一直被使用,直到程序终止或者程序中执行了delete[]p语句(p是new[]语句返回的指针的副本)。
在释放数组之前,系统会按照逆序逐个释放数组中的每个元素。
示例程序中,duplicate_chars函数使用一个指向空字符末尾的字符数组的指针(例如,一个字符的字面量),将数组中的每个元素(包括末尾的空字符)复制到一个新分配的数组中,然后将指针返回到这个新数组的第一个元素。
示例程序:
//释放内存用于数组分配
#包括
#包括
#包括
使用STD:cout;
使用STD:CIN;
使用STD:endl;
size_t strlen(const char* p){
size _ t size=0;
while(*p!=\0){
尺寸;
}
返回大小;
}
模板类输入,类输出
输出副本(输入开始,输入结束,输出目标){
while(开始!=end){
* dest=* begin
}
返回目的地;
}
char* duplicate(const char* p){
size _ t length=strlen(p)1;//为空字符“\0”保留空间
char * result=new char[length];
copy(p,p长度,结果);
返回结果;
}
int main(int argc,char** argv){
char str[]={i , l , o , v , e , U };
STD:string s=duplicate(str);
cout s;
返回0;
}
郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。