一、内存分区模型
1、内存四区
(1)C++程序在执行时,将内存大方向划分为4个区域:
①代码区:存放函数体的二进制代码,由操作系统进行管理的。
②全局区:存放全局变量和静态变量以及常量。
③栈区:由编译器自动分配释放,存放函数的参数值、局部变量等。
④堆区:由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收。
(2)内存四区的意义:不同区域存放的数据将被赋予不同的生命周期,使编程更灵活。
2、程序运行前的情况
(1)在程序编译后,生成了exe可执行程序,未执行该程序前内存分为代码区和全局区。
(2)代码区存放CPU执行的机器指令。
①代码区是共享的,共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可。
②代码区是只读的,使其只读的原因是防止程序意外地修改了它的指令。
(3)全局区存放全局变量和静态变量。
①全局区还包含了常量区,字符串常量和其它常量也存放在此。
②该区域的数据在程序结束后由操作系统释放。
#include<iostream>
using namespace std;
//全局变量(函数体外声明的变量,在所有函数体中都可以使用)
int g_a = 10;
int g_b = 10;
//const修饰的全局变量(全局常量)
const int c_g_a = 10;
const int c_g_b = 10;
int main() {
//创建普通局部变量
int a = 10;
int b = 10;
//静态变量(在普通变量前加static)
static int s_a = 10;
static int s_b = 10;
//常量(字符串常量,const修饰的全局变量,const修饰的局部变量)
cout << "字符串常量的地址为:" << (int)&"Hello world" << endl; //在全局区
const int c_l_a = 10; //const修饰的局部变量
const int c_l_b = 10;
cout << "局部变量a的地址为:" << (int)&a << endl; //局部变量都不在全局区
cout << "局部变量b的地址为:" << (int)&b << endl;
cout << "全局变量g_a的地址为:" << (int)&g_a << endl; //在全局区
cout << "全局变量g_b的地址为:" << (int)&g_b << endl;
cout << "静态变量s_a的地址为:" << (int)&s_a << endl; //在全局区
cout << "静态变量s_b的地址为:" << (int)&s_b << endl;
cout << "全局常量c_g_a的地址为:" << (int)&c_g_a << endl; //在全局区
cout << "全局常量c_g_b的地址为:" << (int)&c_g_b << endl;
cout << "const修饰的局部常量c_l_a的地址为:" << (int)&c_l_a << endl; //局部变量都不在全局区
cout << "const修饰的局部常量c_l_b的地址为:" << (int)&c_l_b << endl;
system("pause");
return 0;
}
3、程序运行后的情况
(1)程序运行后内存会再划分出堆区和栈区。
(2)栈区由编译器自动分配释放,用于存放函数的参数值,局部变量等(子函数不要返回局部变量的地址,子函数返回时栈区开辟的数据由编译器自动释放,局部变量也随之被“删除”,返回其地址基本没有意义;函数将会在下一章详细介绍,这里有个印象即可)。
(3)堆区由程序员分配释放,若程序员不释放,程序结束时由操作系统回收(在C++中主要利用new在堆区开辟内存,利用delete释放内存)。
(4)举例:
①例1:
#include<iostream>
using namespace std;
//栈区数据注意事项——不要返回局部变量的地址
//栈区的数据由编译器管理开辟和释放
int* func(int b) //形参数据也会放在栈区
{
int a = 10; //局部变量存放在栈区,栈区的数据在函数执行完后自动释放
return &a; //(强烈不推荐)返回局部变量的地址
}
int main() {
int b = 0;
int * p = func(b); //接受func函数的返回值
cout << *p << endl; //第一次可以打印正确的数据,因为编译器做了保留
cout << *p << endl; //第二次这个数据就不再保留了,会输出乱码
system("pause");
return 0;
}
②例2:
#include<iostream>
using namespace std;
//在堆区开辟数据
int * func()
{
//利用new关键字可以将数据开辟到堆区
int * p = new int(10); //指针本质也是局部变量,放在栈上,但指针保存的数据放在堆区
return p;
}
int main() {
int * p = func();
cout << *p << endl;
cout << *p << endl;
cout << *p << endl;
system("pause");
return 0;
}
二、指针
1、指针概述
(1)内存编号是从0开始记录的,一般用十六进制数字表示,可以通过指针间接访问内存,也可以利用指针变量保存地址。(下图中a是变量名,p是指针变量名)
(2)指针变量声明的一般形式:
<数据类型> * <变量名>;
①数据类型是指针所指对象的类型,在C++中指针可以指向任何C++类型。
②变量名即指针变量名。
(3)普通变量存放的是数据,指针变量存放的是地址。
(4)指针类型与所指对象之间的关系:
int *px; |
指向整型变量的指针 |
char *pc; |
指向字符型变量的指针 |
char *apc[10]; |
由指向字符的指针构成的数组,即指针数组 |
char( *pac)[10]; |
指向字符数组的指针,即数组指针 |
int *fpi( ); |
返回值为指向整型量的指针的函数,即指针函数 |
int( *pfi)( ); |
指向返回值为整型量的函数的指针,即函数指针 |
int( *p[4][3])( ); |
指针数组,数组中每个元素为指向返回值为整型量的函数的指针 |
int *( *pfpi)( ); |
指向函数的指针,该函数的返回值为指向整型量的指针 |
2、指针和地址
(1)在使用任何指针变量之前必须先给它赋一个指向合法具体对象的地址值,否则该指针是野指针,程序运行时会出现问题。使一个指针指向一个具体对象的方法有:
①使用new运算符(或malloc和alloc等函数)给指针分配一个具体空间。
②将另一个同类型的指针赋给它以获得值。
③通过&运算符指向某个对象。
(2)指针使用两种特殊的运算符——“*”和“&”。
①一元(单目)运算符&用于返回其操作对象的内存地址,其操作对象通常为一个变量名。
②一元(单目)运算符*用于返回其操作数所指对象的值(或者说访问所指对象),其操作对象必须是一个指针,这个操作称为解引用。
(3)举例:
#include<iostream>
using namespace std;
int main() {
//1、定义一个指针
int a = 10;
int * p; //定义指针
p = &a; //让指针记录变量a的地址
cout << "a的地址为:" << &a << endl;
cout << "指针p为:" << p << endl;
//2、使用指针
//可以通过解引用的方式来找到指针指向的内存(指针前加*)
*p = 1000;
cout << "a为:" << a << endl;
cout << "*p为:" << *p << endl;
system("pause");
return 0;
}
3、指针所占内存空间
所有指针类型在32位操作系统下是4个字节。
#include<iostream>
using namespace std;
int main() {
//指针所占内存空间
int a = 10;
int *p = &a;
cout << "sizeof (int *) = " << sizeof(p) << endl;
cout << "sizeof (float *) = " << sizeof(float *) << endl;
cout << "sizeof (char *) = " << sizeof(char *) << endl;
cout << "sizeof (double *) = " << sizeof(double *) << endl;
system("pause");
return 0;
}
4、指针运算
(1)指针和整型量可以进行加减,若p为指针,n为整型量,则p+n和p-n是合法的,同样p++也是合法的,它们的结果同指针所指对象类型的占用字节数相关。
①如若p为指向字符数组第一个元素的指针,则p+1为指向字符数组第二个元素的指针,实际增加1(1是char型数据所占的字节数)。
②如若p为指向整型数组第一个元素的指针,则p+1为指向整型数组第二个元素的指针,实际增加1个整型单位长(假如int类型长度为32位,则实际增加4个字节,可借助sizeof运算符计算某个类型的长度)。
#include<iostream>
using namespace std;
int main() {
//利用指针访问数组中的元素
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int *p = &arr[0]; //int *p = arr; 等价,都是数组首地址
cout << "第一个元素为:" << *p << endl; //利用指针访问第一个元素
p++; //让指针向后偏移四个字节
cout << "第二个元素为:" << *p << endl; //利用指针访问第二个元素
int *p2 = arr;
for (int i = 0; i < 10; i++)
{
cout << *p2 << " ";
p2++;
}
cout << endl;
system("pause");
return 0;
}
(2)若p1、p2为指针,当二者指向同一类型时,可以进行赋值,如:
p2 = p1;
注:该语句使得两指针指向同一空间,若其中一个指针所指空间被删除(释放),则另一个指针所指空间亦会被删除,两个指针均变成野指针。
(3)两个指向同一类型的指针,可进行==、>、<等关系运算,其实就是地址的比较,比如数组中第一个元素的地址会小于第二个元素的地址,以此类推(不同数组的元素地址相互比较基本上没有任何意义)。
#include<iostream>
using namespace std;
int main() {
//字符串逆序输出
char *p, *q, temp;
char s[210] = "zifuchuangnixushuchu";
p = s; //s可代表字符数组s的首地址,也就是第一个元素的地址
q = s + strlen(s) - 1; //q指向字符串s的最后一个字符
while (p < q)
{
temp = *p;
*p = *q;
*q = temp;
p++;
q--;
}
cout << s << endl;
system("pause");
return 0;
}
(4)两个指向同一数组成员的指针可进行相减,结果为两个指针之间相差的元素个数,假如p指向数组头,q指向数组尾,则q-p+1表示数组长度。(指针之间不能相加)
5、指针和数组的联系
(1)在C++中,数组的名字就是指向该数组第一个元素(下标为0)的指针,即该数组第一个元素的地址,也即该数组的首地址。
(2)一般情况下,一个数组元素的下标访问a[i]等价于相应的指针访问*(a+i)。需要注意的是,数组名和指针(变量)是有区别的,前者是常量,即数组名是一个常量指针,而后者是指针变量。
6、空指针、野指针和无类型指针
(1)不指向任何数据的指针称为空指针,其地址值为0,地址0处不能用于存储数据。可以用指针常量NULL(其值为0)来初始化一个指针变量,使之成为空指针。
#include<iostream>
using namespace std;
int main() {
//空指针
//指针变量p指向内存地址编号为0的空间
int *p = NULL;
//cout << *p << endl; //内存编号0 ~255为系统占用内存,不允许用户访问
system("pause");
return 0;
}
(2)指向非法的内存空间(不能访问的空间)的指针称为野指针。定义指针却不初始化,可能就会出现野指针。
#include<iostream>
using namespace std;
int main() {
//野指针
//指针变量p指向内存地址编号为0x1100的空间
//int *p = (int *)0x1100;
//cout << *p << endl;
//在程序中尽量避免出现野指针
system("pause");
return 0;
}
(3)可以用void来定义一个指针变量,称为无类型指针或void指针,例如:
void *pt = NULL;
①无类型指针不与任何特定的数据类型相关联,但却可用来指向任何类型的数据。
②任何类型的指针可以赋值给无类型指针,但反过来却不行。
③无类型指针在一些特殊场合会派上用场。
7、const指针
(1)const修饰指针(const放在*之前)——常量指针:
①常量指针所指向的数据不可改动。
②常量指针本身可以改为指向其它数据。
③定义常量指针时可以不用初始化。
(2)const修饰常量(const放在*之后)——指针常量:
①指针常量所指向的数据可以改动。
②指针常量本身不可以改为指向其它数据。
③定义指针常量时必须初始化。
(3)const既修饰指针,又修饰常量——常量指针常量(前两种指针的综合):
①常量指针常量所指向的数据不可以改动。
②常量指针常量本身不可以改为指向其它数据。
③定义常量指针常量时必须初始化。
#include<iostream>
using namespace std;
int main() {
//1、const修饰指针 常量指针-指针指向的值不可以改
int a = 10;
int b = 10;
const int*p1 = &a;
//*p1 = 20; 这句是错的
p1 = &b;
//2、const修饰常量 指针常量-指针的指向不可以改
int * const p2 = &a;
//p2 = &b; 这句是错的
*p2 = 20;
//3、const修饰指针和常量
const int * const p3 = &a;
//*p3 = 100; 错的
//p3 = &b; 错的
system("pause");
return 0;
}
8、new和delete操作符
(1)使用new获得动态内存空间:
①运算符new用于申请动态存储空间(用的是堆区的空间),它的操作数为某种数据类型且可以带有初值表达式或元素个数。
②new返回一个指向其操作数类型变量的指针。
③使用new对某种类型变量进行动态分配的语法格式如下,其中类型表示要分配的变量类型(如int、char、float等),指针指向new开辟出的空间,指针的类型与要分配的变量类型对应(如int *、char *、float *等)。
<指针> = new <类型>;
④如果需要在分配变量的同时为其赋初值,可以在类型之后加上初值表达式(放在圆括号内),语法格式如下:
<指针> = new <类型>(初值表达式);
⑤运算符new还可以用来对数组进行动态分配,这时需要在数据类型后面添加方括号[],并在其中指明所要分配的数组元素个数(这个元素个数可以是一个变量,而直接定义数组的话,元素个数只能是常量,这是二者最大的区别;new返回数组首元素的地址)。
<指针> = new <类型> [<元素个数>];
⑥当堆区空间不足时,系统可能无法再对new提出的堆内存分配请求给予满足,此时new会返回空指针NULL,表示动态存储分配操作失败,所以最好在每次执行动态存储分配之后都检查一下new返回的指针是否为空,借助if语句,如果返回的指针为空,则须采取必要的措施。
(2)使用delete释放动态内存空间:
①当动态分配的内存空间在程序中使用完毕之后,必须显式地将它们释放,这样做的目的是把闲置不用的堆内存归还给系统,使其可以被系统重新分配。
②在C++程序中由new分配的动态内存空间必须通过delete运算符释放,使用delete对动态分配的单个变量进行释放的语法格式如下,其中指针表示指向单个变量的指针。
delete <指针>;
③使用delete对动态分配的数组进行释放的语法格式如下,其中指针表示指向数组首元素的指针(它必须是由new返回的指向动态内存空间的地址),delete之后的方括号指明将要释放的内存空间中存储着数组元素。
delete [] <指针>;
④new和delete总是成对出现的。
(3)举例:
#include<iostream>
using namespace std;
int * func()
{
//new返回的是该数据类型的指针
int * p = new int(10); //指针本质也是局部变量,放在栈上,但指针保存的数据放在堆区
return p;
}
void test01()
{
int * p = func();
cout << *p << endl;
}
void test02()
{
//在堆区创建含10个整型数据的数组
int * arr = new int[10]; //这个10代表数组有十个元素
for (int i = 0; i < 10; i++)
{
arr[i] = i + 100; //给10个元素赋值
}
for (int i = 0; i < 10; i++)
{
cout << arr[i] << " ";
}
delete[] arr; //释放数组的时候要加中括号
}
int main() {
int * p = func();
test01();
cout << *p << endl;
cout << *p << endl; //堆区的数据由程序员管理开辟以及释放
delete p;
//cout << *p << endl; 内存已经被释放,不能合法访问
test02();
system("pause");
return 0;
}
9、指针作为函数参数
(1)利用指针作函数参数,可以修改实参的值。(函数将会在下一章进行详细介绍,这里有个印象即可)
#include<iostream>
using namespace std;
//值传递
void swap1(int a, int b)
{
int temp = 1;
a = b;
b = temp;
}
//地址传递
void swap2(int *p1, int *p2)
{
int temp = *p1;
*p1 = *p2;
*p2 = temp;
}
int main() {
//
int a = 10;
int b = 20;
swap1(a, b);
cout << a << " " << b << endl;
swap2(&a, &b); //如果是地址传递,可以修饰实参
cout << a << " " << b << endl;
system("pause");
return 0;
}
(2)数组名可作为参数进行传递,当数组名传给函数时,实际上传递的是数组的首地址(即数组第一个元素的地址)。
#include<iostream>
using namespace std;
void bubbleSort(int *p,int a)
{
int temp = 0;
for (int i = 0; i < a -1 ; i++)
{
for (int j = 0; j < a - i - 1; j++)
{
if (*p > *(p + 1))
{
temp = *p;
*p = *(p + 1);
*(p + 1) = temp;
}
p++;
}
p = p + i +1 -a;
}
}
int main() {
//封装一个函数,利用冒泡排序,实现对整型数组的升序排序
//例如数组:int arr[10] = { 4,3,6,9,1,2,10,8,7,5 };
int arr[10] = { 4,3,6,9,1,2,10,8,7,5 };
int a = sizeof(arr) / sizeof(arr[0]); //数组元素个数
bubbleSort(arr, a);
int *p = arr;
for (int i = 0; i < a; i++)
{
cout << *p << " ";
p++;
}
cout << endl;
system("pause");
return 0;
}
三、引用
1、引用的概念
(1)引用是个别名,建立时须用另一个数据对象(如一个变量)的名字进行初始化,以指定该引用所代表的数据对象,此后对引用的任何操作实际上就是对所代表的数据对象的操作,简单说,引用就是给变量起别名,作用跟typedef有点相似。
(2)在类型名后跟引用运算符&以及引用名来创建一个引用(引用名就是一个变量名)。
#include<iostream>
using namespace std;
int main() {
//引用基本语法
int a = 10;
int &b = a; //a的别名为b
cout << a << endl;
cout << b << endl;
b = 100; //用b改值,其实也是给a改值,a和b是同一个
cout << a << endl;
cout << b << endl;
system("pause");
return 0;
}
2、使用引用需要遵循的规则
(1)创建引用时,必须立即对其进行初始化(指针则可以在任何时候被初始化)。
(2)一旦一个引用被初始化为一个对象的引用,它就不能再被改变为对另一个对象的引用(指针则可以在任何时候改变为指向另一个对象)。
(3)不可能有NULL引用。必须确保引用是对具体合法的对象的引用(即引用应和一块合法的存储空间关联)
#include<iostream>
using namespace std;
int main() {
int a = 10;
int b = 20;
//引用必须初始化
//int &c ; 这显然是不行的
//引用一旦初始化后,就不可以更改了
int &c = b;
//int &c = a; 这样也是不行的,当然,像c = a这种是赋值操作,不是更改引用
cout << a << endl;
cout << b << endl;
cout << c << endl;
system("pause");
return 0;
}
3、引用的本质
引用并不会占用额外的存储空间,其本质是在c++内部实现的一个指针常量。
#include<iostream>
using namespace std;
//发现是引用,转换为 int* const ref = &a;
void func(int& ref) {
ref = 100; // ref是引用,转换为*ref = 100
}
int main() {
int a = 10;
//自动转换为 int* const ref = &a; 指针常量是指针指向不可改,这也说明为什么引用不可更改
int& ref = a;
ref = 20; //内部发现ref是引用,自动帮我们转换为: *ref = 20;
cout << "a:" << a << endl;
cout << "ref:" << ref << endl;
func(a);
system("pause");
return 0;
}
4、用引用传递函数参数
(1)函数传参时,可以利用引用的技术让形参修饰实参,这样可以简化指针修改实参。
(2)通过引用参数产生的效果同按地址传递是一样的,引用的语法更清楚简单。
#include<iostream>
using namespace std;
//1、值传递
void mySwap01(int a, int b)
{
int temp = a;
a = b;
b = temp;
}
//2、地址传递
void mySwap02(int *a, int *b)
{
int temp = *a;
*a = *b;
*b = temp;
}
//3、引用传递
void mySwap03(int &a, int &b) //引用传递,形参也会修饰实参
{
int temp = a;
a = b;
b = temp;
}
int main() {
int a = 10;
int b = 20;
mySwap01(a, b);
cout << "a=" << a << " b=" << b << endl;
mySwap02(&a, &b);
cout << "a=" << a << " b=" << b << endl;
mySwap03(a, b);
cout << "a=" << a << " b=" << b << endl;
system("pause");
return 0;
}
(3)在函数形参列表中,可以加const修饰形参,防止形参改变实参。
#include<iostream>
using namespace std;
//常量引用使用场景:用于修饰形参,防止误操作
void showValue(const int& val)
{
//val = 1000; 如果不加const,万一在函数体内val发生变化,由于是引用传递,主函数中的a也会发生变化
cout << val << endl;
}
int main() {
int a = 100;
int &ref = a; //引用必须引一块合法的内存空间
const int & ref2 = 10; //加上const之后,编译器将代码修改为int temp =10 ; const int & ref2 = temp ;
//ref2 = 20; 加入const之后变为只读,不可修改
showValue(a);
system("pause");
return 0;
}
5、引用做函数返回值
(1)引用是可以作为函数的返回值存在的,但注意不要返回局部变量引用。
(2)引用做函数返回值时,函数的调用可以为左值。
#include<iostream>
using namespace std;
//引用做函数的返回值
//不要返回局部变量的引用
//函数的调用可以作为左值
int& test01()
{
int a = 10; //局部变量,存放在栈区
return a;
}
int& test02()
{
static int a = 10; //静态变量,存放在全局区(或者说堆区),程序结束后再由系统释放
return a;
}
int main() {
int &ref1 = test01();
cout << "ref1= " << ref1 << endl;
//cout << "ref= " << ref << endl; //此时a的内存已经释放,不能访问了
int &ref2 = test02();
cout << "ref2= " << ref2 << endl;
cout << "ref2= " << ref2 << endl;
test02() = 1000; //函数的调用可以作为左值,test02返回的是a的引用,这里相当于a = 1000
cout << "ref2= " << ref2 << endl;
cout << "ref2= " << ref2 << endl;
system("pause");
return 0;
}