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();
}

#


53(补充)字符串_数组_作为参数

普通数组退化 vs. 字符串字面量退化#

1. 本质上的统一:都是“数组退化” (Array Decay)#

在 C++ 编译器眼中,无论是你定义的数组还是手写的字符串,它们的原始身份都是固定长度的数组

  • int arr[5] 的原始类型是:int[5]
  • "abc" 的原始类型是:const char[4](包含隐藏的 \0

退化规则:当它们作为参数传递给接收指针的函数时,会执行相同的操作——丢失长度信息,仅留下首元素的内存地址。

2. 它们的三大核心区别#

虽然底层逻辑相同,但在内存布局和使用习惯上,它们有显著差异:

维度普通数组 (如 int[])字符串字面量 (如 "abc")
结束标志。必须额外传递 size 参数。。自带“哨兵” \0 (Null Terminator)。
内存区域通常在 栈 (Stack),可读可写。只读数据区 (Static Data),不可修改。
退化后的类型int[5] \rightarrow int*const char[4] \rightarrow const char*

3. 详细拆解#

区别 A:哨兵机制 (The Terminator)#

  • 普通数组:函数拿到 int* 后,完全不知道哪里是数组的终点。如果你不传 size,函数极易越界。
  • 字符串:虽然也退化成了 const char*,但函数可以通过不断向后寻找 \0 来确定长度(这就是 strlen 的工作原理)。

区别 B:内存安全性#

  • 普通数组
  int arr[] = {1, 2, 3};
  arr[0] = 9; // 完全合法,你拥有这块内存的写权限
  • 字符串字面量:通常在只读数据区(Static/Data Segment)。它虽然退化成了 const char*,但你绝对不能尝试通过指针去修改它,否则程序会直接崩溃。

区别 C:退化后的类型#

  • int a[5] \rightarrow 退化为 int*
  • "abc" (即 const char[4]) \rightarrow 退化为 const char*(注意多了一个 const,因为字面量不可修改)。

string及string引用传参对比#

void test1(const std::string& s ) {}
void test2(const std::string s ) {}

int main()
{
  test1("abc");
  test2("abc");
}
特性const std::string& sconst std::string s
内存身份函数内部的 s 只是外部对象的别名函数内部的 s 是一个全新的副本
拷贝次数仅隐式转换产生的 1 次临时构造转换构造 + 传参拷贝(共 1-2 次)
推荐程度极高(行业标准)不推荐(除非你打算在函数里修改它)
安全性保证不会在函数内产生冗余副本容易引发不必要的性能损耗

53模板

template,能让你定义一个待编译的模版 让编译器用一组规则为你编译代码

当我写一个函数时,我在函数中使用一个模板,我实际做的事类似蓝图的东西,当我决定调用那个函数时,指定一些参数,他们决定了实际放入模板的代码

简单例子#

#include <iostream> 

//template表明这是个编译时求值的模板,这不是实际的函数,只有在调用时才会创建并编译成源代码
//可以从实际参数中隐式获取T
template<typename T>
void Print(T value)
{
	std::cout << value << std::endl;
} 
int main()
{
	Print(5);
	//"5.5"被隐式转为const char[4]
	Print("5.5");
	Print(3.7);

	std::cin.get();
}

额外补充#

模板定义调用 Print(“5.5”) 时 T 的类型说明
void Print(T value)const char*发生退化。传递的是地址。
void Print(T& value)const char[4]不发生退化。传递的是数组的引用。
void Print(const T& value)char[4]不发生退化。T 本身是 char[4]。
// 它代表:我只接受一个含有 4个char的数组的“引用”
void Print(const char (&value)[4]) {
    // sizeof(value) == 4
    // 编译器知道这个数组刚好就是 4 个字节
}
//数组的首地址
void Print(const char* value) {
    // sizeof(value) == 8 (在64位系统上)
    // 编译器不知道这里有 4 个字符
}

例子2#

#include <iostream> 

//template表明这是个编译时求值的模板,这不是实际的函数,只有在调用时才会创建并编译成源代码(才会实际创建函数)
//可以从实际参数中隐式获取T
template<typename T>
//和上面一个意思
//template<class T>
void Print(T value)
{
	std::cout << value << std::endl;
}

int main()
{
	//显示指定T
	Print<int>(5);
	//让编译器自己推断T的类型
	Print(5);

	std::cin.get();
}

例子3#

52处理多返回值

这篇讨论的是如何在函数返回两个值

cherno推荐的方法是创建一个结构体并返回它

代码预备#

为了演示,本篇使用了string,为了顺便探讨性能(视频中没提到),我写了一个string.h 和string.cpp打印相关信息

//String.h
#pragma once // 防止头文件被重复包含
#include <iostream>

class String
{
private:
    char* m_Buffer;
    unsigned int m_Size;

public:
    // 默认构造函数
    String() : m_Buffer(nullptr), m_Size(0) {}

    // 构造函数
    String(const char* string);

    // 拷贝构造函数(深拷贝)
    //拷贝构造函数 (String a = b;):当你在创建一个新的对象,并用已有的对象初始化它时调用。
    String(const String& other);

    // 析构函数
    ~String();

    // 运算符重载
    char& operator[](unsigned int index);

    // 赋值运算符重载 (a = b;):当两个对象都已经存在(已经构造完毕),你只是想把其中一个的值覆盖给另一个时调用。
    String& operator=(const String& other); 

    // 友元函数:重载 << 运算符
    friend std::ostream& operator<<(std::ostream& stream, const String& string);

    // 测试函数
    unsigned int& mytest() { return m_Size; } // 简单的内联函数可以直接写在类内
};
#include "String.h"
#include <cstring> // 必须包含 cstring 才能使用 strlen 和 memcpy

// 构造函数
String::String(const char* string)
{
	m_Size = strlen(string);
	m_Buffer = new char[m_Size + 1];
	memcpy(m_Buffer, string, m_Size);
	m_Buffer[m_Size] = '\0';
}

// 拷贝构造函数
String::String(const String& other)
	: m_Size(other.m_Size)
{
	std::cout << "Copied String!" << std::endl;
	m_Buffer = new char[m_Size + 1];
	memcpy(m_Buffer, other.m_Buffer, m_Size + 1);
}

//赋值运算符 
String& String::operator=(const String& other)
{
	std::cout << "Assignment Operator Called!" << other << std::endl;

	// 1. 自赋值检查 (防止 s1 = s1 的骚操作)
	if (this == &other)
		return *this;

	// 2. 释放当前对象旧的内存,防止内存泄漏
	delete[] m_Buffer;

	// 3. 申请新空间并拷贝内容
	m_Size = other.m_Size;
	m_Buffer = new char[m_Size + 1];
	memcpy(m_Buffer, other.m_Buffer, m_Size + 1);

	// 4. 返回对象自身,支持链式赋值 (a = b = c)
	return *this;
}

// 析构函数
String::~String()
{
	delete[] m_Buffer;
}

// 下标运算符重载
char& String::operator[](unsigned int index)
{
	return m_Buffer[index];
}

// 友元函数定义(注意:定义时不需要加 String::,也不需要加 friend 关键字)
std::ostream& operator<<(std::ostream& stream, const String& string)
{
	stream << string.m_Buffer;
	return stream;
}

方法1:使用引用#

#include <iostream>
#include "String.h"

void ParseShader(String& vertexSource, String& fragmentSource)
{
	//假设vs,fs最终计算值是abc、def
	String vs = "abc";
	String fs = "def";

	//这两行代码执行的是 字符串赋值(内部会拷贝)
	vertexSource = vs;
	fragmentSource = fs;
}

int main()
{
	String vs, fs;
	ParseShader(vs, fs);
	std::cout << vs << std ::endl;
	std::cout << fs << std::endl;
	std::cin.get();
}
/*
Assignment Operator Called!abc
Assignment Operator Called!def
abc
def

*/

方法2:使用指针#

#include <iostream>
#include "String.h"

void ParseShader(String* vertexSource, String* fragmentSource)
{
	//假设vs,fs最终计算值是abc、def
	String vs = "abc";
	String fs = "def";

	//这两行代码执行的是 字符串赋值(内部会拷贝)
	if (vertexSource) {
		*vertexSource = vs;
	}
	if (fragmentSource) {
		*fragmentSource = fs;
	} 
}

int main()
{
	String vs, fs;
	ParseShader(&vs, &fs);
	//这种方式允许空指针
	//ParseShader(nullptr, &fs);
	std::cout << vs << std::endl;
	std::cout << fs << std::endl;
	std::cin.get();
}
/*
Assignment Operator Called!abc
Assignment Operator Called!def
abc
def
*/

(同类型)方法3:返回(数组)指针#

#include <iostream>
#include "String.h"

String* ParseShader()
{
	//假设vs,fs最终计算值是abc、def
	//vs,fs先在栈上创建(但String成员m_Buffer是指向堆)
	String vs = "abc";
	String fs = "def";

	//这种语法在c++20才能编译通过
	return new String[]{ vs,fs };
} 

int main()
{ 
	//这里不知道数组有多大
	String* result =ParseShader();

	std::cout << result[0] << std::endl;
	std::cout << result[1] << std::endl;
	std::cin.get();
}
/*
Assignment Operator Called!abc
Assignment Operator Called!def
abc
def
*/

分析: