C++(week10): C++基础:第一章 C++与C

发布于:2024-07-01 ⋅ 阅读:(21) ⋅ 点赞:(0)

文章目录

零、C++历史、C++概述

1.C++历史

C++之父:Bjarne Stroustrup



2.C++概述

(1)C++基础部分、C++提升部分

在这里插入图片描述


(2)默认模板

cd ~/.vim/plugged/prepare-code/snippet

在这里插入图片描述


#include <bits/stdc++.h>
#include <iostream> 
#include <cstdio>
#include <vector>
#include <string>
#include <algorithm>

using namespace std;
using std::cout;
using std::endl;

int main()
{ 
    cout << "Hello world" << endl;
    return 0;
}

(3)C++可以用小括号进行初始化赋值,而C不可以

//assignment.c
#include <func.h>

int main()
{
    int x = 1;
    int y(x); //错误,C语言中仅可以使用 int y = x; 进行初始化赋值 
    printf("y = %d\n", y);
    return 0;
}

//assignment.cpp
#include <iostream> 
using std::cout;
using std::endl;

int main()
{
    int x = 1;
    int y(x);  //C++中赋值不仅可以采用等号,还可以使用小括号
    cout << y << endl;
    return 0;
}

(4)头文件顺序

①自定义头文件
②C语言头文件
③C++头文件
④第三方库头文件



一、C++与C

1.命名空间

1.什么是命名空间?
namespace命名空间,是程序员命名的内存取余,程序员根据需求将一些实体存放到命名空间中,与全局实体分隔开,从而减少或避免命名冲突问题。


2.命名空间的作用:避免命名冲突
在命名空间中可以声明实体、定义实体,但是不能使用实体。命名空间中的实体一定在命名空间之外使用,可以理解为命名空间只是用来存放实体

在这里插入图片描述


(3)使用命名空间中的变量或函数的3种方法

(1)作用域限定符

std::endl;

(2)using编译指令

using namespace std;

using编译指令只建议局部使用, 使用该方式,会引入命令空间中的全部实体,可能造成命名冲突。
若不清楚一个命令空间中具体有什么实体,尽量避免使用using编译指令,因为可能造成同名问题。

(3)using声明机制

using std::cout;
using std::endl;
using Ed::print;  //只写函数名
using Ed::num;

#include <iostream> 
using std::cout;
using std::endl;

namespace ed{
int num = 1;

void print(){
    cout << "wxf::print()" << endl;
    cout << "num = " << num << endl;
}

}//end of namespace ed

namespace Ed
{
int num = 10;
 
void print(){
    cout << "Ed::print()" << endl;
    cout << "num = " << num << endl;
}

}//end of namespace Ed

void test1(){
    ed::print();	//使用作用域限定符
}

void test2(){
    using namespace Ed;  //使用using编译指令
    print();
}

int main()
{
    test1();
    test2();
    return 0;
}

using声明机制有屏蔽效果,会屏蔽全局变量:但是使用using编译指令会和全局变量冲突
在这里插入图片描述


三种对比:
推荐使用第三种,using声明语句。
第一种:命名空间 + 作用域限定符,最精准,最不容易冲突,但最繁琐
第二种:using编译指令,最简单,但最容易冲突

在这里插入图片描述


4.命名空间的嵌套
命名空间还可以嵌套命名空间。
命名空间中可以存放各种实体。


(5)匿名命名空间 (简称匿名空间)

(1)作用:限定命名空间只能在本文件中使用,不能够跨文件使用 (类似static、局部变量和局部函数)
(2)使用方法:加上作用域限定符(没有空间名),或 直接使用
(3)匿名空间不要定义与全局空间中同名的实体,否则会冲突,或者 无法访问到匿名空间的变量(只能取到全局实体)。

在这里插入图片描述

namespace {
//...
} 

cout << val << endl;
cout << ::val << endl;
func();
::func();

6.跨文件使用命名空间:extern
可以跨文件/跨模块调用的:
①自定义头文件中声明,目标文件调用,#include " "
②全局变量和全局函数 + extern (外部引入声明) 【只引入几个,开销比使用头文件少。但比较混乱,没有指定是从哪个文件引用,联合编译时容易重定义】
③有名空间

在这里插入图片描述

在这里插入图片描述

//externA.cc
namespace wd
{
int val = 300;
void display(){
cout << "wd::display()" << endl;
}
}//end of namespace wd

//
//externB.cc
namespace wd
{
extern int val;			//extern
extern void display();	//extern
}

void test0(){
cout << wd::val << endl;
wd::display();
}

7.命名空间可以多次定义
但里面定义的内容不能重复


8.规范
在这里插入图片描述



2.const关键字:只读变量,有类型

const修饰的变量称为const常量,之后不能修改其值。(const常量本质还是变量,但是加了只读属性,初始化之后不能再修改其值。有类型。)

1.const常量在定义时必须初始化

const int number1 = 10;
int const number2 = 20;

const int val;//error 常量必须要进行初始化

(2)const与宏定义的区别

除了这种方式可以创建常量外,还可以使用宏定义的方式创建常量

#define NUMBER 1024

发生时机不同:宏定义预处理时进行字符串替换,const常量编译时赋予变量只读属性。
类型和安全检查不同:宏定义没有类型检查,只是字符串替换const常量有类型,编译器会进行类型检查。

在这里插入图片描述


(3)const修饰指针

①指向常量的指针、常量指针

(1)const在*的左边,称为指向常量的指针 (pointer to const):
可以修改指针的指向,但是不能通过指针修改其指向的值

const int * p 或 int const * p
//指向常量的指针才能去指向常量
const int num = 10;
const int * p = &num; //不加const,则普通指针不能指向常量

(2)const在*的右边,称为 常量指针(const pointer):
可以修改指针指向的值,不可以修改指针的指向

int * const ptr;
int num1 = 1;
int num2 = 2;
int * const p = &num1;
*p = num2;
p = &num2; //❌

(3)双重const限定的指针,只读指针

const int * const p = &num1;//指向和指向的值皆不能进行修改

①常量指针:不能修改指针的指向,但可以修改其指向的变量的值,int * const p
②指向常量的指针:不能通过该指针修改其指向的变量的值,但可以修改该指针的指向,const int * p 或 int const *p


拓展:

②数组指针、指针数组

(1)数组指针指向数组的首地址,控制范围是整个数组的空间

int (*p)[5] = &arr;  //数组指针
//数组指针:数组的指针
void test(){
    int arr[5] = {1, 2, 3, 4, 5};
    int * p1 = &arr[0];
    int * p2 = arr;
    cout << "p1 = &arr[0] = " << p1 << endl;
    cout << "p2 = arr = " << p2 << endl;
    cout << "p2+1 = arr+1 = " << p2+1 << endl;

    //数组指针指向数组的首地址,控制范围是整个数组的空间
    //int * p3 = &arr;      //error
    int (*p4)[5] = &arr;    //ok
    cout << "p4 = &arr = " << p4 << endl;
    cout << "p4+1 = &arr+1 = " << p4+1 << endl;

    for(int i = 0; i < 5; i++){
        cout << (*p4)[i] << endl;
    }
}

(2)指针数组:数组中保存的元素都是指针变量

//指针数组:元素为指针变量的数组
void test2(){
    int num1 = 9, num2 = 8, num3 = 5;
    int * p1 = &num1;
    int * p2 = &num2;
    int * p3 = &num3;
    int* arr[3] = {p1,p2,p3};
    for(int i = 0; i < 3; i++){
        cout << *arr[i] << endl;
    }
}

③函数指针与指针函数

(1)函数指针 (Function Pointer)
函数指针是指向函数的指针。它存储的是函数的地址。可以被用来动态调用不同的函数,这在实现回调函数或者高度可配置的代码中非常有用。

void print(int x){
    cout << "print:" << x << endl;
}

void test(){
    //定义函数指针时,要确定其指向的函数的返回值类型和参数
    //1.省略形式
    void (*p)(int) = print;
    p(985);
    //2.完整形式
    void (*p2)(int) = &print;
    (*p2)(211);
}

完整内容查看:https://blog.csdn.net/Edward1027/article/details/138532112


(2)指针函数 (Pointer Function):返回类型是指针类型的函数
①错误示范:永远不要返回指向当前栈帧的指针
在这里插入图片描述
②正确示范:

//指针函数:返回值类型是指针类型的函数
//注意:返回的指针指向的内容的生命周期要长于函数
int num = 100;

int* func(){
    int * p = &num;
    return p;
}

int main()
{   
    test();
    int* p = func();
    cout << "*p = " << *p << endl;
    return 0;
}

函数指针:是一个指针,它指向函数。
指针函数:是一个函数,它返回一个指针。



3.new/delete表达式

(1)new

1.new申请普通变量

①new ()里不填,则申请的每个元素被初始化为相应类型的默认值
②建议写(),确保一定可以初始化

void test(){
	int * p = (int *)malloc(sizeof(int));
	*p = 100;
	free(p);
	
	int * p1 = new int(100); //申请堆上int型初始化为100
	cout << *p1 << endl;

	int * p2 = new int();   //初始化为0
	int * p3 = new int;     //初始化为0。不推荐这样写,不写小括号,无法在所有平台确保初始化

	char * pc1 = new char();  //默认初始化为空字符 \0
	char * pc2 = new char('a');
}

2.new申请数组 + { } 标准初始化列表

//开辟数组空间,其中每个元素被初始化为相应类型的默认值
int * p = new int[5]();  //默认初始化为0
int * p2 = new int[5]{1,2,3}; //标准初始化列表: 1 2 3 0 0 

3.在C++中使用C风格字符串的方式

const char *p = "hello";
char * ps = new char[strlen(p) + 1]();  //不加1,可能访问越界,覆盖其他信息

在这里插入图片描述

拓展:输出流运算符对char*有默认重载效果,会自动访问指针保存的地址上的内容

cout << ps << endl;  //这里输出的不是地址值,而是字符串的内容

(2)delete

使用delete进行内存回收

三种回收方式:对应三种申请方式,malloc、new、new[ ]。怎么申请的空间,就怎么回收。

//1.malloc申请整数变量
int *p = (int *)malloc(sizeof(int));  //使用 malloc 分配内存
*p = 10;  //手动初始化

//2.malloc申请整型数组
//在C语言中,malloc 只负责分配内存,不会进行初始化,因此需要手动初始化数组
int n = 5;
int* arr = (int*)malloc(n * sizeof(int));
for(int i = 0; i < n; i++){
	arr[i] = i+1;
}

//3.使用 new 分配内存并初始化
int *p1 = new int(10); 

//4.使用new申请数组
int n = 5;
int* arr = new int[n] = {1,2,3,4,5};
free(p);	  //回收变量
free(arr);	  //回收数组
delete p1;	  //回收变量
delete [] ps; //回收数组

总结:
malloc 需要手动分配内存并进行类型转换和初始化。
new 直接分配并初始化,语法更加简洁和安全。


(3)malloc/free 和 new/delete 的区别

1.malloc/free是库函数,new/delete是表达式
2.malloc返回 通用指针类型void*,需要强转。而new/delete表达式是类型安全的,直接返回相应类型的指针。
3.malloc申请的空间不会进行初始化,是有脏数据的。而new表达式申请的空间会直接初始化
4.malloc的参数是字节数,而new表达式不需要传递字节数,会根据相应类型自动获取空间的大小

在这里插入图片描述


(4)检测内存泄露:valgrind

0.安装:

sudo apt install valgrind

1.检测内存泄露:
(1)简陋版

valgrind --tool=memcheck ./可执行程序名

(2)详细版,显示行号 (编译时记得加-g选项)

g++ xxx.cpp -o 可执行程序名 -g  //需要加-g选项
valgrind --tool=memcheck --leak-check=full ./可执行程序名  //能提示行数

2.为了使用时不输入这么长的指针,可以取别名
在这里插入图片描述


3.内存泄露分类:
①definitely:绝对泄露
②indirectly:间接泄露
③possibly:可能泄露
④still reachable:不确定要不要回收
⑤suppressed:被编译器自动回收了,不需要管


(5)安全回收,指针回收后,还要置空指针

int * p = (int*)malloc(sizeof(int));
*p = 100;
free(p);
p = nullptr;  //安全回收



4.引用 (最重点)

(1)引用的概念、定义

1.引用是已定义变量的别名

//定义方式:    类型 & ref = 变量;
int number = 2int & ref = number;

①声明引用的同时,必须对引用进行初始化,否则编译时报错
②引用一经绑定,无法更改绑定

void test(){
    int num = 100;
    int & ref = num;//声明ref时进行了初始化(绑定)
    //int & ref2; //error
    cout << num << endl;
    cout << ref << endl;
    cout << &num << endl;
    cout << &ref << endl;
}

2.引用是C++与C的一个重要区别


(2)引用的本质:const pointer

引用变量是一个常量指针:不能修改指向,但能修改指针指向变量的值。
占据8字节。但编译器阻止了对它的任何访问。


(3)引用与指针的联系与区别

1.联系:
(1)引用和指针都有地址的概念,都用来间接访问变量
(2)引用的底层实现是受限制的指针 const pointer

2.区别:
(1)引用必须初始化;指针可以不初始化
(2)引用不能修改绑定;普通指针可以修改指向。
(3)

在这里插入图片描述


(4)引用的使用场景

引用作为函数的参数 (重点)

1.引用作为函数的参数,避免复制

void swap(int x, int y){//值传递,发生复制
    int temp = x;
    x = y;
    y = temp;
}

void swap2(int * px, int * py){//地址传递,不复制
    int temp = *px;
    *px = *py;
    *py = temp;
}

//调用swap3时,就是用实参初始化形参
//即int & x = a; int & y = b;
void swap3(int & x, int & y){//引用传递,不复制
    int temp = x;
    x = y;
    y = temp;
}

void test1(){
	int a = 1, b = 2;
	swap3(a, b);
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
}

int main(){
	test1();
	return 0;
}

在这里插入图片描述


2.参数传递的方式:①值传递 ②指针传递(地址传递) ③引用传递
如果函数中需要改变实参本身的内容,值传递就无法实现了,需要引用传递(或者地址传递)。


3.常引用
(1)常引用(const引用),即双const限定的只读指针。
(2)想要避免复制,又想要保证变量在函数内部不被修改,就要使用 常饮用 const int & x
在这里插入图片描述


引用作为函数的返回值

1.要求:当以引用作为函数的返回值时,返回的变量其生命周期一定是要大于函数的生命周期的,即当函数执行完毕时,返回的变量还存在。

2.目的: return时避免复制的开销

int  func(){
    //...
    return a;   //在函数内部,当执行return语句时,会发生复制 
}   

int & func2(){
     //...
    return b;   //在函数内部,当执行return语句时,不会发生复制
}  

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3.注意事项:
(1)不要返回局部变量的引用
如果使用引用作为函数的返回值,要确保引用所绑定的变量本体的生命周期比函数更长
在这里插入图片描述

(2)不要轻易返回一个堆空间变量的引用,非常容易造成内存泄漏。[所以谨慎使用这种写法,要有完善的内存回收机制]

每次调用func4,都会new,但是无法delete。
在这里插入图片描述

必须再用一个引用ref来delete
在这里插入图片描述


(5)引用总结

1.在引用的使用中,单纯给某个变量取个别名没有什么意义,引用的目的主要用于在函数参数传递中,解决大块数据或对象的传递效率和空间不理想的问题。
2.用引用传递函数的参数,能保证参数传递中不产生副本,提高传递的效率,还可以通过const的使用,保证了引用传递的安全性。
3.引用与指针的区别是,指针通过某个指针变量指向一个变量后,对它所指向的变量间接操作。程序中使用指针,程序的可读性差;引用底层仍然是指针,但是编译器不允许访问到这个底层的指针,逻辑上简单理解为——对引用的操作就是对目标变量的操作。可以用指针或引用解决的问题,更推荐使用引用



5.强制转换

(1)C风格的强制类型转换:①不安全,可以在任意两个类型之间进行转换。不合理的转换不会编译报错;②形式普通,(目标类型),难以查找。

TYPE a = (TYPE)EXPRESSION;

(2)C++的强制类型转换:static_cast,const_cast,dynamic_cast,reinterpret_cast


(1)static_cast

最常用的类型转换符,在正常状况下的类型转换, 用于将一种数据类型转换成另一种数据类型,如把int转换为float。不允许进行不合理的类型转换,会编译报错。

目标类型 转换后的变量 = static_cast<目标类型>(要转换的变量)

在这里插入图片描述
在这里插入图片描述


(2)const_cast

const_cast用来去除const属性,但只能对引用或指针使用。基本不用

在这里插入图片描述
在这里插入图片描述


奇怪的点:同一个地址,无法修改const值,但是复制出来的值修改了,地址相同
因为变量本身是常量,修改了但是无法写回内存。
在这里插入图片描述


(3)dynamic_cast

dynamic_cast:该运算符主要用于基类和派生类间的转换,尤其是向下转型的用法中


(4)reinterpret_cast

reinterpret_cast:功能强大,慎用(也称为万能转换)

该运算符可以用来处理无关类型之间的转换,即用在任意指针(或引用)类型之间的转换,以及指针与足够大的整数类型之间的转换。由此可以看出,reinterpret_cast的效果很强大,但错误的使用reinterpret_cast很容易导致程序的不安全,只有将转换后的类型值转换回到其原始类型,这样才是正确使用reinterpret_cast方式



6.函数重载

(1)函数重载的概念

1.函数重载的作用:减少函数名的数量。

重载函数通常用来命名一组功能相似的函数,这样做减少了函数名的数量,对于程序的可读性有很大的好处
重载:overload

2.函数重载的定义:
在同一作用域内,可以有一组具有相同函数名,不同参数列表的函数,这组函数被称为重载函数

①C++ 允许多个函数拥有相同的名字,只要它们的参数列表不同就可以,这就是函数重载(Function Overloading)。借助重载,一个函数名可以有多种用途。
②注意:C 语言中不支持函数重载,C++才支持函数重载。


(2)实现函数重载的条件

**函数参数的数量、类型、顺序任一不同则可以构成重载。**只有返回类型不同,参数完全相同,是不能构成重载的。 (原因:名字改编)

在这里插入图片描述


(3)函数重载的实现原理

函数重载的原理:C++的编译器进行名字改编

实现原理: 名字改编(name mangling)——当函数名称相同时 ,会根据参数的类型、顺序、个数进行改编

在这里插入图片描述

在这里插入图片描述

分析:C++的函数重载提供了一个便利,以前C语言要想实现各种不同类型参数的计算需要定义多个不同名字的函数,在调用函数时要注意参数的信息和函数名匹配。

C++有了函数重载,想要对不同类型的参数进行计算时,就可以使用同一个函数名字(代码层面的同名,编译器会处理成不同的函数名)。

缺点在于,C++编译器进行编译时比C的编译器多了一个步骤,效率有所降低。


(4)extern"C"

C++编译器的名字改编,降低了效率。若希望C++代码中,有部分代码 按照C编译器进行编译,从而提高效率。 可以使用混合编程,即extern"C"。

1.用extern"C"修饰单个函数

extern "C" void func(){

}

2.如果多个函数都希望用C的方式进行编译,或是需要使用C语言的库文件,都可以放到 { }中

extern "C"
{
//...
}



7.函数默认参数

(1)默认参数的作用

1.给函数参数赋默认值后就可以进行缺省调用,但是传入的参数优先级高于默认参数

void func(int x = 0, int y = 0){
    cout << "x = " << x << endl;
	cout << "y = " << y << endl;
}

void test0(){
    func(24,30);
    func(100);	
    func();
}

(2)默认参数的声明

通常是将默认值的设置放在声明中而不是定义中


(3)默认参数的顺序规定

2.多个默认参数,【从右边往左】进行连续赋值
在这里插入图片描述


(4)默认参数与函数重载可能冲突(二义性):声明中用默认值

如果函数的声明与定义是分离的,还想要使用默认参数。通常将默认值放入声明中,而不是定义中。

尽量按照 声明、定义、调用 的顺序,去写函数

在这里插入图片描述

缺省调用 与 函数重载冲突:不知道填两个参数的调用,是真正两个参数,还是缺省调用。
在这里插入图片描述
在这里插入图片描述


8.bool类型

1.非0为true,0为false

2.sizeof(bool)是1


9.内联函数 (inline函数)

为什么要用小函数来封装小操作?有什么好处?
①可读性高
②易于修改

缺点:函数调用有开销。

那么如何避免这种调用函数的开销呢?有宏函数和内联函数两种方式。


(1)什么是内联函数 (inline函数)

1.为何要提出内联函数的概念?inline函数有什么好处?
①内联可以像宏函数一样避免函数调用的开销
②内联函数又像普通函数,不会像宏函数那样容易导致错误
所以,内联函数综合了普通函数和宏函数的优点,避免了两者的缺点。

inline int max(int x, int y){
	return x > y ? x : y;
}

2.inline只是建议,编译器在编译阶段会进行检查,不一定采纳。

3.比较短小且常用的代码适合用inline函数


(2)宏函数与内联函数

1.联系:
(1)这两者都是为了将较短小的函数进行字符串文本替换,从而避免函数调用开销。(“函数调用开销”是指参数压栈、跳转、退栈和返回等操作)

2.区别:
(1)宏不可调试,而内联函数在 调试版本(Debug版本) 中没有真正内联,在 发行版本(Release版本) 中才会真正内联。
(2)宏是在预处理时进行字符串替换;而内联函数是在编译时进行进行字符串替换,且有类型检查,比较安全。


宏函数出错的例子
例1:不加括号
在这里插入图片描述

例2:自增操作
在这里插入图片描述
用正常函数max,应该是4和5


(3)内联函数的注意事项

1.声明和实现分开的情况:
inline要放在实现前面,只加在声明前面不行。
建议声明和实现都加inline。

在这里插入图片描述

如下,foo函数不能成为内联函数:

inline void foo(int x, int y);//该语句在头文件中

void foo(int x, int y){//实现在.cpp文件中
	//...
}

2.inline函数在头文件中必须有定义。
(1)inline函数的声明和实现不能放在不同的文件中。
如果要把inline函数声明在头文件中,则必须把函数定义也写在头文件中。若头文件中只有声明没有实现,被认为是没有定义替换规则。

(2)但如果普通函数的实现也写在头文件中,编译的时候会重定义。



10.异常处理 (了解)

异常是程序在执行期间产生的问题。C++ 异常是指在程序运行时发生的特殊情况,比如尝试除以零的操作。异常提供了一种转移程序控制权的方式。C++ 异常处理涉及到三个关键字:try、catch、throw.

①try:检测异常
②throw:抛出异常
③catch:捕获异常


(1)throw

抛出异常即检测是否产生异常,在 C++ 中,其采用 throw 语句来实现,如果检测到产生异常,则抛出异常。该语句的格式为:

throw 表达式;

先定义抛出异常的规则(throw),异常是一个表达式,它的值可以是基本类型,也可以是类;

double division(double x, double y)
{
	if(y == 0)
		throw "Division by zero condition!";
	return x / y;
}

(2)try-catch语句块

1.try-catch语句块的语法如下:

try {
//语句块
} catch(异常类型) {	//注意:catch的是类型,不是具体信息
//具体的异常处理...
} ...
catch(异常类型) {
//具体的异常处理...
}

2.try-catch语句块的catch可以有多个,至少要有一个,否则会报错。

  • 执行 try 块中的语句,如果执行的过程中没有异常拋出,那么执行完后就执行最后一个 catch块后面的语句,所有 catch 块中的语句都不会被执行;
  • 如果 try 块执行的过程中拋出了异常,那么拋出异常后立即跳转到第一个“异常类型”和拋出的异常类型匹配的 catch 块中执行(称作异常被该 catch 块“捕获”),执行完后再跳转到最后一个catch 块后面继续执行。

3.举例:

//exception.cpp 
#include <iostream> 
using std::cout;
using std::endl;

double division(double x, double y){
    if(y == 0){
        throw "Division by zero condition!";
    }
    if(x == 0){
        throw 4.8;
    }
    return x / y;
}

//具体对象,则输出的是throw的值
void test1(){
    try{
        /* division(100, 0); */
        division(0,100);
    }catch(const char * msg){ //catch匹配的是异常类型
        cout << msg << endl;
    }catch(double a){
        cout << a << endl;
    }
}

//没指定具体对象,则只匹配catch所对应的类型
void test2(){
    try{
        division(100, 0); //发生异常, try中接下来的语句不会执行
        division(0, 100);   
    }catch(const char *){
        cout << "catch(const char *)" << endl;
    }catch(double){
        cout << "catch(double)" << endl;
    }
}

int main()
{
    test1();
    cout << "----------------" << endl;
    test2();   
    return 0;
}

输出结果:

4.8
----------------
catch(const char *)

4.Google编程规范中不推荐使用异常处理,try、catch将代码割裂了,破坏了整体性、



11.内存布局 (内存分区) (重要)

1.文字介绍:
(1)内核态空间
(2)用户态空间
栈区:操作系统控制,由高地址向低地址生长,通常为0x7ff
堆区:程序员分配,由低地址向高地址生长,堆区与栈区没有明确的界限,通常为0x5
全局/静态区:读写段(数据段),存放全局变量、静态变量
文字常量区:只读段,存放程序中直接使用的常量,如const char * p = “hello”; hello这个内容就存在文字常量区
程序代码区:只读段,存放函数体的二进制代码


2.图片介绍
在这里插入图片描述
在这里插入图片描述

3.代码介绍
(1)堆比栈的地址更低
(2)堆区是动态分配的,地址不一定连续
(3)全局变量在堆之下
(4)静态变量 和 全局变量是混合存放的。先定义的在低地址。

//memory_layout.cpp
#include <stdio.h>
#include <iostream> 
using std::cout;
using std::endl;

int global_num = 1;

int main()
{
    int num = 1;          //栈变量
    cout << "栈变量1的地址 :" << &num << endl; //输出栈变量的地址:0x7ffc2050a440
    int num2 = 2;          //栈变量
    cout << "栈变量2的地址 :" << &num2 << endl; //输出栈变量的地址:0x7ffc2050a444

    int *p = new int(1);  //堆变量
    cout << "堆变量的地址  :" << p << endl;    //输出堆变量的地址:0x56bb74428280
    int *p2 = new int(2);  //堆变量
    cout << "堆变量2的地址 :" << p2 << endl;    //输出堆变量的地址:0x56bb744282a0
    //对比发现,堆变量的地址比栈变量低
    //即,堆变量是低地址,栈变量是高地址

    cout << "全局变量的地址:" << &global_num << endl; //0x56bb73306010

    static int static_num = 1;
    cout << "静态变量的地址:" << &static_num << endl; //0x56bb73306014
    //对比可以发现,全局变量和静态变量是混合存放的。略低于堆区
    
    const char * pstr = "hello";
    cout << pstr << endl;   //hello    cout会默认重载char *
    printf("文字常量区的地址:%p\n", pstr);   //0x56012fd1ddab
    cout << "文字常量区的地址:"<< static_cast<void *>(const_cast<char*>(pstr))<<endl;
    //发现文字常量区,比全局静态区的地址更低
    
    printf("main函数的地址:%p\n",&main);
    //发现程序代码段,比文字常量区更低
    return 0;
}

在这里插入图片描述



12.C风格字符串

(1)数组形式

1.如果用数组形式,注意留出一位给终止符\0

char arr[6] = {'h' ,'e', 'l', 'l', 'o'}; //空间足够的话,最后一位可以不手动写'\0',会自动填充

在这里插入图片描述


(2)指针形式

2.如果用指针形式,直接定义为const char * ,C++代码中标准C风格字符串的写法。

在这里插入图片描述

在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到