64多维数组

  • 多维数组,其实就是由数组作为元素,构成的数组

二维数组与三维数组#

#include <iostream>

int main()
{
	//存储50个整数, 指针存储这50个整数的第一个数的地址
	//50*4=200字节
	int* array = new int[50];

	//A2D, 2D数组,50个[整数指针]
	//分配50个[整数指针]; 32位系统,一个指针4字节
	//50*4=200字节
	int** a2d = new int*[50];

	//都是分配了200个字节,4个字节一组,总共50组

	//类型,其实就是在设置处理这些属性的语法,比如 int* array,
	//int** a2d等等
	array[0] = 0;//处理的是整数
	a2d[0] = nullptr;//处理的是指针

	//有空间存储50个指针,这些指针共占200字节,之后我们可以遍历
	//它们,并将每个指针都设置为指向一个数组. 其实最终得到的就是50个数组
	//二维数组: 有一个数组,包含着(50个)[数组的内存地址] 
	for (int i = 0; i < 50; i++)
	//为50个数组元素(指针)处理操作
	{
		//让每一个数组元素(指针)的值,等于new(50个元素)出来的数组的首地址
		a2d[i] = new int[50];
	}


	//分配50个(二维数组)
	int*** a3d = new int**[50];
	//遍历每一个二维数组
	for (int i = 0; i < 50; i++)
	{
		//初始化二维数组
		a3d[i] = new int* [50];
		//遍历a3d[i]这个数组的50个元素
		for (int j = 0; j < 50; j++)
		{
			int** ptr = a3d[i];
			//让每个元素指向new出的50个元素的数组
			ptr[j] = new int[50];
		}
	}
	//a3d[0]:得到一个二级指针数组
	//a3d[0][0]:得到一个指针数组
	//a3d[0][0][0]:得到整数
	a3d[0][0][0] = 0;

	std::cin.get();
}

详解二维数组#

#ifdef LY_EP64
#include <iostream>

int main()
{
	//创建一个数组,每个数组元素是指针
	int** a2d = new int* [50];

	//对50个元素赋值
	for (int i = 0; i < 50; i++)
	{
		//对第i个指针元素赋值,每个指针元素
		//指向新创建的数组
		a2d[i] = new int[50];

	}
	//第1个数组的第一个元素赋值为0
	//左边的0是指针的索引
	//右边的0,1,2是整数的索引
	a2d[0][0] = 0;
	a2d[0][1] = 0;
	a2d[0][2] = 0;

	//没有这种语法,编译报错
	//delete[][] a2d;


	for (int i = 0; i < 50; i++)
	{
		//删除所有内部的数组
		delete[] a2d[i];
	}
	//这个删除的是,保存指针元素的那个数组(最外层)
	delete[] a2d;

	std::cin.get();
}
#endif

内存碎片化#

#ifdef LY_EP64
#include <iostream>

int main()
{ 
	//包含5个指针元素的数组
	int** a2d = new int* [5];
	 
	for (int i = 0; i < 5; i++)
	{ 
		//每个元素都包含五个数组
		a2d[i] = new int[5];
	} 

	//设置元素的值
	for (int y = 0; y < 5; y++)
	{
		for (int x = 0; x < 5; x++)
		{
			a2d[y][x] = 2;
		}
	}

	//这25个元素并不是一个连续的能
	//容纳25个整数的缓冲区,而是5个独立
	//的缓冲区,每个缓冲区能容纳5个整数
	//这5个缓冲区,被分配到内存中的随机位置

	for (int i = 0; i < 5; i++)
	{
		//删除所有内部的数组
		delete[] a2d[i];
	}

	//这个删除的是,保存指针元素的那个数组(最外层)
	delete[] a2d;

	std::cin.get();
}

#endif;
  • 如果我们必须遍历,每次遍历5个整数后(访问25个整数中的每一组),然后转到数组的下一行(下一个整数数组),到内存中的另一个位置读取/写入数据,这可能会导致缓存未命中,这意味着从内存中获取数据时会浪费时间(如果他们分配得很近可能不会缓存未命中)
    • 如上所示,这样遍历这25个整数,比在内存中连续分配一个25个整数的一维数组慢很多,因为那段内存是连续的
    • 当你编程优化时,当你处理内存时,优化时最重要的一件事就是优化内存访问
    • 如果能够在内存中紧凑存储要访问的内存,并且通过某种方式摆放,以获得更多的缓存命中,使程序运行更快
  • 缓存学习包括:CPU缓存如何工作

代替二维数组#

#ifdef LY_EP64
#include <iostream>

int main()
{
	//这是一个栈上的原生二维数组,且内存中连续(对比int** a2d = new int* [5];方式)
	//物理形态:[ 1, 2, 3, 4, 5, 6, 7, ... 25 ] 
	//特点:编译器通过公式 Address + (row x 5 + col) x 4 直接定位。没有额外的寻址操作。(字节定位)
	int a[5][5] = { 1,2,3,4,5,6,7,8,9,10, 11,12,13,14,15,16,17,18,19,20, 21, 22, 23, 24, 25 };
	for (int i = 0; i < 5; i++)
		for (int j = 0; j < 5; j++)
			std::cout << a[i][j] << std::endl;


	//使用一维数组(连续的一块内存)完成二维数组5 * 5 的数组功能
	int* array = new int[5 * 5];
	for (int y = 0; y < 5; y++)
	{
		for (int x = 0; x < 5; x++)
		{
			array[x + y * 5] = 2;
		}
	}

	std::cin.get();
}

#endif;

建议#

  • 避免使用二维数组,如果存储一个位图,有一幅图像的所有元素,会把图像(正方形或长方形)看成二维的,但不代表要把它存为二维数组,建议存为一维数组

为什么推荐“平铺的一维数组”而不是int a[5][5]#

  • 动态需求:实际开发中(如加载一张图片),你往往在运行时才知道宽度和高度。这时你无法使用 int a[W][H] (W和H如果是动态则编译失败),只能用 new
  • 统一性:如果你习惯了 array[y * width + x],那么无论数组是在栈上(int array[25];)还是堆上(int* array = new int[width * height];),你的逻辑 底层逻辑 都是统一的。
    • 对比:如果你用原生语法,栈上是 a[y][x],堆上如果你用 int** 也是 a[y][x] 如果用int* array = new int[width * height];就没办法用这种形式. 和前者甚至不🙆‍♀️ ,但它们的底层指令完全不同(一个计算偏移,一个跳转指针),且不能互相传递。
  • 传递方便:将 int a[5][5] 作为参数传递给函数很麻烦(必须指定列数,如 void func(int arr[][5])),而传递一个 int* 则非常简单通用
    • 编译器必须知道“每一行有多少个元素”,否则它无法计算出a[i][j]的内存地址。

63计时

  • 计时:完成某项操作或执行特定代码所需的时间
  • c++17加入了 chrono

计时简单例子#

#ifdef LY_EP63

#include <iostream> 
//引入了本节所需要的大部分内容
//跨平台计时
#include <chrono>
//线程相关
#include <thread>

int main()
{
	//为了使用1s表示1秒的用法
	using namespace std::literals::chrono_literals;

	//获取当前时间
	//auto--> std::chrono::steady_clock::time_point
	auto start = std::chrono::high_resolution_clock::now();

	//模拟耗时
	//指定大概睡眠1秒,会有其他开销
	std::this_thread::sleep_for(1s);
	auto end = std::chrono::high_resolution_clock::now();
	
	//算出实际时长
	//也可以用auto
	std::chrono::duration<float> duration = end - start;
	std::cout << duration.count() << "s" << std::endl;
	
	std::cin.get();
}
#endif 

解释字面量1s#

1s 被称为 用户定义字面量 (User-defined literals),代表一个 std::chrono::seconds 对象,其值为 1。字面量本质上是调用了一个特殊的运算符函数

// 编译器实际执行的操作(示意)
operator""s(1);

在c++的标准库的头文件中,它的定义大致如下:

// 简化后的底层实现
constexpr chrono::seconds operator""s(unsigned long long _Val) noexcept {
    return chrono::seconds(_Val);
}
  • operator"":这是定义字面量的关键字
  • s:这是后缀名称。
  • constexpr:意味着这个转换在编译期就能完成,没有任何运行时开销(Runtime overhead)。

给函数计时#

#ifdef LY_EP63

#include <iostream> 
//引入了本节所需要的大部分内容
//跨平台计时
#include <chrono>
//线程相关
#include <thread>

struct Timer
{
	std::chrono::time_point<std::chrono::steady_clock> start, end;
	std::chrono::duration<float> duration;


	Timer()
	{
		start = std::chrono::high_resolution_clock::now();
	}

	~Timer()
	{
		end = std::chrono::high_resolution_clock::now();
		duration = end - start;//可以省略不存储结束值

		//1000.0f不是重载,而是编译器硬编码的,1000f会编译报错
		float ms = duration.count() * 1000.0f;
		//24ms
		//std::cout << "Timer took " << ms << "ms" << std:: endl;
		//21ms
		std::cout << "Timer took " << ms << "ms\n";// << std::endl;
	}
};

void Function()
{
	//作用域(函数)内创建Timer对象,整个(函数)作用域(结束后)都会被计时
	Timer timer;
	for (int i = 0; i < 100; i++)
		std::cout << "Hello" << std::endl;
}

int main()
{ 
	Function();
	std::cin.get();
}
#endif 

其他#

  • visual studio也自带分析工具
  • 或者其他任何集成开发环境也许自带分析工具
  • 可以在源代码中修改使其具有类似的计时(分析)工具
  • 还可以用来展示每个函数运行时长的图表,以及那个函数调用了哪个函数

62多线程

  • std::cin.get() 等待用户输入的那段时间,什么也做不了。此时就可以在后台开另一个线程做其他事

从这篇开始,代码都会写在同一个项目中,使用Mainxx_yy.cpp的类似名字。由于同一个项目有多个main函数(同一篇都好几个),所以文件最顶端添加宏进行判断,visual studio中对项目配置如下

函数声明与类创建[区别]#

#ifdef LY_EP62

#include <iostream>
#include <thread>

class Test
{
public:
	Test()
	{
		std::cout << "Test()" << std::endl;
	}

	Test(int k)
	{
		std::cout << "Test(int)" << std::endl;
	}
};
void DoWork()
{

}

int main()
{
	//声明一个类Test的对象
	Test t{};

	// 这是一个函数声明,声明一个函数名
	// 为 t1,没有参数,返回类型
	// 是 Test
	Test t1();

	// 这是一个函数声明,声明一个函数名
	// 为 worker,没有参数,返回类型
	// 是 int
	std::thread worker1();
	
	//声明一个线程对象
	std::thread worker2{};

	//创建线程并立即启动DoWork内的操作
	std::thread worker(DoWork);
	worker.join();

	std::cin.get();
}
#endif

线程简单例子#

#ifdef LY_EP62

#include <iostream>
#include <thread>
 
void DoWork()
{

}

int main()
{
	// 这是一个函数声明,声明一个函数名
	// 为 worker,没有参数,返回类型
	// 是 int
	std::thread worker1();

	//创建的是一个“空”线程对象,内部没有关联
	// 任何实际的执行函数,也不启动线程
	std::thread worker2{};

	//创建线程并立即启动DoWork内的操作
	std::thread worker(DoWork);

	//...启动->到join这期间的代码并
	//不会阻塞

	//主线程会等待
	//主线程会停在 join() 这一行,像在车站等
	// 朋友一样,直到子线程这辆车“到站”并合
	// 二为一,主线程才会继续往下走。
	worker.join();

	//子线程完成前不会执行该行代码
	std::cin.get();
}
#endif
  • join的作用:让当前线程(通常是主线程)停下来,进入阻塞状态,直到子线程执行完毕为止。
  • 主线程启动一个工作线程,工作线程执行任务,然后在主线程上等待工作线程完成所有任务

子线程无限循环#

#ifdef LY_EP62

#include <iostream>
#include <thread>

void DoWork()
{
	while (true)
	{
		std::cout << "working..." << std::endl;
	}
}

int main()
{ 
	std::thread worker(DoWork); 

	worker.join();

	//子线程完成前不会执行该行代码
	std::cin.get();
}
#endif

阻止无限循环#

#ifdef LY_EP62

#include <iostream>
#include <thread>

// 显式包含时间库
//cherno视频里没有却能运行,可能是有些编译器中,
// <thread> 头文件为了实现 sleep_for 等功能,
// 其内部已经写了 #include <chrono>。(ai回答)
#include <chrono> 

static bool s_Finished = false;

void DoWork()
{ 
	using namespace std::literals::chrono_literals;
	while (!s_Finished)
	{
		std::cout << "working..." << std::endl;
		std::this_thread::sleep_for(1s);
	}
}

int main()
{
	std::thread worker(DoWork);

	//主线程自己阻塞自己了
	std::cin.get();
	//用户按下回车后
	s_Finished=true;

	//按下回车后,子线程或许刚进入
	//while语句块{ ,且还没到达打印语句
	//所以可能子线程要再打印一行,之后下
	//一次循环才不会进入


	worker.join();
	std::cout << "===child thread finished~~==" << std::endl;

	//子线程完成前不会执行该行代码
	std::cin.get();
}
#endif
/* 输出
working...
working...
working...
working...
working...
		 //这就是用户按下的回车行
working...
===child thread over~==

*/

子线程配合join可以用来做清理工作,主线程等待子线程清理完毕后再退出或执行某些操作

61(60)避免使用命名空间

例子#

代码简化

也可以只在函数内部(或者其他作用域)声明

但是有个缺点,没法直接判断vector是哪来的,标准的?还是自己写的

代码示例#

#include <iostream> 
#include <string>

namespace apple {
	void print(const std::string& text)
	{
		std::cout << text << std::endl;
	}
}

namespace orange {
	void print(const char* text)
	{
		std::string temp = text;
		std::reverse(temp.begin(), temp.end());
		std::cout << temp << std::endl;
	}
}

using namespace apple;
using namespace orange;

int main()
{
	//::  这个是作用域解析运算符  
	apple::print("Hello");

	//寻找更为匹配的 orange::print
	print("hello");
	/*
Hello
olleh
	*/


	std::cin.get();
}

建议#

  • 任何时候都不要在头文件(xx.h)里面放入using namespace xx;,因为没人知道它会被include到哪里
  • 尽量只有自己的库才用它
  • 尽量在更小的作用域内使用 if、函数、最多是在文件

60(61)命名空间

  • 命名空间是什么、有什么用、何时使用、何时不使用、他们有什么用
  • 主要用途是避免命名冲突

60(61)命名空间#

#include <iostream> 
#include <string>

namespace apple {
	void print(const char* text)
	{
		std::cout << text << std::endl;
	}
}

namespace orange {
	void print(const char* text)
	{
		std::string temp = text;
		std::reverse(temp.begin(), temp.end());
		std::cout << text << std::endl;
	}
}

int main()
{
	//::  这个是作用域解析运算符 
	apple::print("Hello");
	std::cin.get();
}

如果这段代码没有命名空间,那么就存在两个函数签名一模一样的函数,编译都过不去 两个同名且相同的符号(类,函数,变量,常量),同一文件中会编译错误,不同文件则链接错误

C语言没有命名空间语法,所以比如glfw库中的函数都是以glfw开头(避免冲突)

如果没有命名空间#

#include <iostream> 
#include <string>

//namespace apple {
	void apple_print(const char* text)
	{
		std::cout << text << std::endl;
	}
//}

//namespace orange {
	void orange_print(const char* text)
	{
		std::string temp = text;
		std::reverse(temp.begin(), temp.end());
		std::cout << text << std::endl;
	}
//}

int main()
{ 
	apple_print("Hello");
	std::cin.get();
}

代码编写风格#

namespace apple { namespace functions {
	void print(const char* text)
	{
			std::cout << text << std::endl;
	}
}}

这么做仅仅是为了减少缩进

58_59函数指针、lambda表达式

本篇聊聊来自C语言的那种原始风格函数指针,后续会讲C++处理函数指针的方法,以及lambda表达式

  • 函数指针,是一种把函数赋给变量的方式
  • 函数只是个符号,并不能进行任何逻辑计算,但是可以拿来调用
  • 可以接受传参,如果返回非void可以得到相应结果
  • 函数可以赋值给变量,函数也可以作为参数传递给其他函数
  • 函数是cpu指令,存储在我们的二进制文件中的某处
  • auto function = HelloWorld;,获取cpu指令的内存地址并赋值给function

58_59函数指针、lambda表达式#

#include <iostream>  

void HelloWorld()
{
	std::cout << "HelloWorld!" << std::endl;
}

int main()
{
	//function的类型->void (*function)()
	auto function1 = &HelloWorld;
	//&可以省略,因为有隐式转换
	auto function = HelloWorld;
	//相当于
	void (*function2)() = HelloWorld;

	function();
	(*function)();
	function1();
	(*function1)();
	function2();
	(*function2)();
	std::cin.get();
}

隐式转换#

  1. 为什么不需要 &? 当你直接使用函数名 HelloWorld 时,编译器会将其视为该函数在内存中的起始地址。
  • auto function = HelloWorld;:编译器看到函数名,自动将其转换为函数指针。
  • auto function = &HelloWorld;:显式地取函数地址。

这两行代码生成的机器码通常是完全一样的。

  1. 只有一种情况“必须”注意 虽然对于普通函数两者等价,但在处理类成员函数 (Member Functions) 时,规则会变严:
  • 普通函数: & 可选。
  • 类成员函数: 必须使用 & 并且加上类名限定。

例如:auto func = &MyClass::MemberFunction;(这里不能省略 &)。

57标准数组

  • 本篇讲解std::array,c++标准模板库中的标准数组类
  • 这是静态数组,所谓的静态,即不会扩容缩小,数组一旦创建就要定义大小 std::vector不同 、元素类型。
#include <iostream>
#include <array>
#include <algorithm>

void PrintArray1(int* array, unsigned int size)
{
	for (int i = 0; i < size;)
	{

	}
}

//如果不知道数组大小,使用模板。让编译器自己推断
template<size_t N>
void PrintArray(const std::array<int, N>& data)
{
	// 在这里,N 会根据传入的数组自动推导
	for (size_t i = 0; i < data.size(); i++)
	{
		std::cout << data[i] << std::endl;
	}
}

void PrintArray(const std::array<int,5>& data)
{
	for (int i = 0; i < data.size();)
	{

	}
}

int main()
{
	std::array<int, 5> data;
	std::cout << data.size() << std::endl;
	data[0] = 2;
	data[4] = 1;
	//data[5] = 3;

	//std::array能使用迭代器
	std::sort(data.begin(), data.end());

	int data2[5];
	data[1] = 3;
}
  • 标准数组和普通数组,都是存放在栈上 标准向量是在堆上
  • 有警告

data.size()如何得知大小#

std::array 的大小(即数字 5)并不存储在程序运行时的内存里,是通过模板推导和常量替换实现的。

56auto

  • 让cpp自动推导新声明变量的类型
  • c++在一定程度上会转换为弱类型语言

简单例子#

#include <iostream> 

int main()
{
	auto x = 1;//int
	auto y = 1.0;//double
	int a = 5;
	auto b = a;//int
	auto c = y;//double
	auto d = 3L;//long
	auto e = 5.3f;//float
	auto f = "aa";//const char* //常量字符指针
	std::cout << x << std::endl;
	std::cout << y << std::endl;
	std::cout << a << std::endl;
	std::cout << b << std::endl;
	std::cout << c << std::endl;
	std::cin.get();
}

进阶例子#

#include <iostream>
#include <string>

std::string GetName1()
{
	return "Cherno";
}

//返回值应该是const,修改项目设置后才能
// 通过编译: c/c++-Language-Conformancemode-No
char* GetName2()
{
	return "Cherno";
}

int main()
{
	auto a = 'a';

	std::string name1 = GetName1();

	//string有一个可接受字符指针的隐式构造函数
	std::string name2 = GetName2();
	name2.size();

	//如果没有ide显示没办法一眼看出name3的类型
	auto name3 = GetName2(); //char*
	//name3.size();//显然这里编译会失败

	std::cout << *name3 << std::endl;//"C"

	std::cin.get();
}

补充-接收字符指针如何构造string#

#include <iostream>
#include <cstring> // 为了使用 strlen 和 strcpy

class SimpleString {
private:
    char* m_Buffer;     // 指向堆内存的指针
    unsigned int m_Size; // 字符串长度

public:
    // 这就是那个“隐式构造函数”
    // 它接收一个 const char* 指针
    SimpleString(const char* string) {
        std::cout << "[Log] 调用了构造函数,接收指针: " << (void*)string << std::endl;
        
        m_Size = strlen(string);          // 1. 计算长度
        m_Buffer = new char[m_Size + 1];  // 2. 在堆上申请内存(+1 是为了放 '\0')
        memcpy(m_Buffer, string, m_Size + 1); // 3. 将内容从只读区拷贝到堆区
    }

    // 析构函数:防止内存泄漏
    ~SimpleString() {
        delete[] m_Buffer;
    }

    // 辅助函数,方便打印
    void Print() const {
        std::cout << m_Buffer << std::endl;
    }
};

char* GetName2() {
    return (char*)"Cherno"; // 字符串字面量在只读区
}

int main() {
    // 发生了隐式转换!
    // 编译器发现 GetName2() 返回 char*,而你需要 SimpleString
    // 于是它自动调用了 SimpleString("Cherno")
    SimpleString name = GetName2(); 

    name.Print();

    std::cin.get();
}

相对有用的场景#

#include <iostream>
#include <string>
#include <vector>

//返回值应该是const,修改项目设置后才能
// 通过编译: c/c++-Language-Conformancemode-No
char* GetName()
{
	return "Cherno";
}

//普通返回类型
auto GetName1()
{
	return "Cherno";
}

//后置返回类型1
auto GetName2() -> const char* {
	return "Cherno";
}

//后置返回类型2
//编译器要先看到a,b的类型,才能推断返回类型
template <typename T, typename U>
auto Multiply(T a, U b) -> decltype(a* b) {
	return a * b;
}

int main()
{
	std::vector<std::string> strings;
	strings.push_back("apple");
	strings.push_back("orange");

	//迭代器,是一个经过精心设计的类,内部最核心的成
	// 员通常就是一个指向容器中某个位置的指针
	for (std::vector<std::string>::iterator it = strings.begin();
		it != strings.end(); it++)
	{
		std::cout << *it << std::endl;
	}

	std::cout << "======1=====" << std::endl;

	//类型过于冗长,用auto替代
	for (auto it = strings.begin();
		it != strings.end(); it++)
	{
		std::cout << *it << std::endl;
	}
	std::cout << "======2=====" << std::endl;

	//类型别名 (Type Aliasing) [现代C++风格]:,支持模板别名(模板化using)
	using UIterator = std::vector<std::string>::iterator;

	//类型别名 (Type Aliasing) [传统C风格]
	typedef std::vector<std::string>::iterator TIterator;

	//类型过于冗长,用auto替代
	for (UIterator it = strings.begin();
		it != strings.end(); it++)
	{
		std::cout << *it << std::endl;
	}
	std::cout << "======3=====" << std::endl;

	//类型过于冗长,用auto替代
	for (TIterator it = strings.begin();
		it != strings.end(); it++)
	{
		std::cout << *it << std::endl;
	}
	std::cout << "======4=====" << std::endl;
	std::vector<std::string> vectors;
	vectors.push_back("hello");
	vectors.push_back("world");

	//s--->const std::string& s
	for (const auto& s : vectors) {
		std::cout << s << std::endl;
	}
	


	std::cin.get();
}

常见的使用场景:

55宏Macro

该集只是简单介绍一下

  • 用预处理器对某些操作进行==类似宏处理实现自动化 ==

  • 编译C++程序时,首先要进行预处理器处理

    • 以#开头的都叫做预处理器指令
    • 会被求值,然后处理好。再传递给编译器进行实际编译等操作—文本编辑阶段(可以控制实际输入到编译器的代码)
  • 工作阶段:宏是在 预处理(Preprocessing) 阶段处理的。这意味着在编译器真正看到你的代码并进行语法分析之前,宏就已经被处理完毕了。

  • 核心功能:宏本质上是 查找与替换(Find and Replace)。预处理器会扫描你的源代码,找到宏的名称,并将其替换为你定义的代码块或值。

  • 预处理器(Preprocessor)先处理,模板(Templates)后处理。

    • 预处理器处理完后,编译器再进行模板实例化
  • 建议:没有必要频繁的使用高级特性

  • 宏是“翻译单元”私有的

    • 在 C++ 编译过程中,每个 .cpp 文件都是独立编译的,这被称为一个翻译单元(Translation Unit)。
    • 如果你想在多个 .cpp 文件中使用同一个宏,你应该把它放在一个头文件(.h)中,然后在需要的 .cpp 文件里#include它。
#include <iostream>
#include <string>
int main()
{
	//#define在改行代码之后,所以这行编译不过去
	//WAIT;

	#define WAIT std::cin.get()
	//等待从控制台读取一行 
	WAIT;
}

代码被预处理器处理后编译器看不出和普通代码有任何区别,我们用宏仅仅是改变了源代码的生成方式

例子1#

#include <iostream>
#include <string>

//等待从控制台读取一行 
//如果改行末尾带了;分号,也会被替换
#define WAIT std::cin.get()
#define OPEN_CURLY { 
#define SAY_HELLO std::cout << "Hello1" << std::endl

int main()
OPEN_CURLY

	SAY_HELLO;
	WAIT;
}

例子2#

#include <iostream>
#include <string>

#define LOG(x) std::cout << x << std::endl

int main()
{

	LOG("Hello");
	std::cin.get();
}

分别处理调试和正式发布#

Debug模式下预处理定义了 LY_DEBUG;

54栈内存和堆内存

栈和堆何时被使用,为何被使用,以及如何运作

  • 程序运行时,操作系统会把整个程序加载到内存中,同时分配大量物理内存
  • 栈和堆是内存中实际存在的两个区域
    • 栈具有预定义的大小,通常约2兆字节
    • 堆也有预定义默认值,随着应用程序运行,它会增长变化
    • 我们需要一个地方存储运行程序所需的数据,如局部变量、或者从文件读取数据
  • 请求内存,也称内存分配

接下来看看在栈上分配一个整数,和在栈上分配其他数据的差异。与实际c++代码的堆相比

代码#

#include <iostream>
#include <string>

struct Vector3
{
	float x, y, z;
	Vector3()
		:x(10), y(11), z(12)
	{

	}
};

int main()
{
	{
		//在栈上分配内存
		int value = 5;
		int array[5];
		array[0] = 1;
		array[1] = 2;
		array[2] = 3;
		array[3] = 4;
		array[4] = 5;
		Vector3 vector;

		std::cout << &value << std::endl;
		std::cout << array << std::endl;
		std::cout << &vector << std::endl;
		
	}//作用域结束后,该作用域内栈上分配所有内容都被弹出(内存被释放)
	//在栈上分配内存和释放内存都只是移动栈指针,几乎没有性能损耗

	//new关键字分配堆内存
	int* hvalue = new int;
	*hvalue = 5;
	int* harray = new int[5];
	harray[0] = 1;
	harray[1] = 2;
	harray[2] = 3;
	harray[3] = 4;
	harray[4] = 5;
	//()可以省略
	Vector3* hvector = new Vector3();

	//必须手动释放内存
	delete hvalue;
	delete[] harray;
	delete hvector;

	std::cin.get();
}

#