c++ 入门

本文详细介绍了C++编程中的关键步骤,包括预处理(如#include处理)、编译(生成抽象语法树和机器码)、链接(合并对象文件并解决函数和变量定位),以及变量和函数的内存管理。同时,深入探讨了指针、引用、类和结构体的用法,强调了静态成员和作用域的概念。此外,还讲解了调试技巧和头文件的使用策略。
摘要由CSDN通过智能技术生成

001 c++ 如何工作

  • 任何以 # 开头的语句,都是预处理语句,所谓的预处理语句,在编译之前,就已经被处理了

  • 关键字 include:找到 <> 文件(通常称为“头文件”),然后将 <> 中的所有内容拷贝到现在的文件里

  • main()比较特殊,虽然它的返回值类型是 int,但它不一定需要返回值,如果不设置返回值,默认返回 0

  • << 运算符叫做重载运算符,可以看成是一个函数(重载运算符 == 函数

  • 解决方案平台(Solution Platforms):编译代码的目标平台;
    x86:目标平台是 windows32 位(生成 32 位的 windows 应用程序)

  • 预处理后,我们的文件将被编译,编译器将所有 c++ 代码 -> 二进制的机器码

  • 源文件.cpp --编译–> 目标文件.obj --链接–> 可执行文件.exe

  • 链接:将 .obj 文件链接(合并)到一起,编成一个可执行文件。比如函数声明,是不同.cpp之间的合并。这里强调一下,只有函数调用的时候,才会产生链接,声明的时候,是不会进行链接的。声明只是在函数被调用的时候,能够找到合适的链接函数

  • 头文件不会被编译,头文件拷贝过来时,已经被编译过了;
    编译一个单独的文件时,不会 link。单个文件编译快捷键:ctrl + F7

Main.cpp

# include <iostream>  # 这一条语句其实包含了 50623 行代码,所以 Main.cpp 文件才会很大

void Log(const char* message);

int main()
{
	// std::cout << "Hello World!" << std::endl;  // 将字符串 Hello World! 推送到 cout 流中,然后打印到终端;endl 告诉终端,跳到下一行
	Log("Hello World!!");
	std::cin.get();  // 程序暂停执行,直到按下 enter 键
}

Log.cpp

#include <iostream>

void Log(const char* message)
{
	std::cout << message << std::endl;
}




002 c++ 编译器如何工作

  • 编译器本质工作:将我们的文本文件 -> 目标文件,便于后续的链接

  • 我们先按照编程语言的规则,将代码进行记号化和解析成编译能理解和推理的格式,然后编译器对所有的代码进行预处理,这儿会产生抽象语法树。编译器的工作就是将我们的代码转换成常量数据或指令,一旦编译器生成这个 AST ,它可以生成实际的机器码

  • 抽象语法树(abstract syntax code,AST)是源代码的抽象语法结构的树状表示,树上的每个节点都表示源代码中的一种结构,这所以说是抽象的,是因为抽象语法树并不会表示出真实语法出现的每一个细节。比如,嵌套括号被隐含在树的结构中,并没有以节点的形式呈现

  • 编译器只会编译 .cpp 的 c++ 文件,这些 cpp 文件被称为翻译单元(一个 CPP 文件不一定等于一个翻译单元,如果项目较小,那么每个 CPP 文件都是一个翻译单元)

  • 实际上,没有所谓的文件,编译器根本不 care 文件不文件,文件只是提供给编译器源代码的一种方式而已

  • 下面的例子,主要想说明的就是,编译器在处理头文件时,会把头文件中的所有内容都复制到当前文件

  • 强调一下,我们平时在写代码的时候,要尽可能的简洁,尽可能的优化我们的代码,比如将 Math.cpp 中的 int result = a * b; 一行去掉,直接 return a * b; 编译在工作时,会节省四行代码

Math.cpp

int Math(int a, int b)
{
	int result = a * b;
	return result;
#include "EndBrace.h"

EndBrace.h

}




003 c++ 链接器如何工作

  • 链接的主要工作:找到每个符合和函数在哪,并把他们链接起来

  • 源文件被编译之后,每个文件都是单独的翻译单元,他们之间没有任何关系,不能交互

  • 下面的例子很好的说明了链接的作用,Multiply() 定义了,即使你在当前的翻译单元里没有调用 Multiply(),但是链接器会认为你在其他的文件里会用到这个函数,所以,它还是会被进行链接。函数内部的函数调用,也会进行链接,所以调用的 Log() 和 Log.cpp 文件中的 Logr() 不对应,就会产生链接错误

  • 我们可以使用 static 函数,直接规定死,Multiply() 只在当前文件下使用,不让它和其他变量或函数进行链接,就会避免上述错误

Math.cpp

# include <iostream>

void Log(const char* message);

// static int Multiply(int a, int b)
int Multiply(int a, int b)
{
	Log("Multiply");
	return a * b;
}

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

Log.cpp

#include <iostream>

void Logr(const char* message)
{
	std::cout << message << std::endl;
}
  • 下面介绍一下,函数定义重复,导致的链接错误

  • 这里其实会报错的,Math.cpp 和 Log.cpp 都包含了头文件 # include “Log.h”,也就是说,Log() 的定义被复制了两遍,产生重复链接错误。如果我们将 Log() 的修饰符改为 static 或 inline ,则会消除链接错误

  • 还有一种解决办法就是,在 Log.h 中只声明 Log(),然后在 Log.cpp 中定义 Log(),这样也不会产生链接错误

Log.h

# include <iostream>

void Log(const char* message)
{
	std::cout << message << std::endl;
}

Log.cpp

# include "Log.h"

void InitLog()
{
	Log("Initialized Log");
}

Math.cpp

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

static int Multiply(int a, int b)
{
	Log("Multiply");
	return a * b;
}

int main()
{
	std::cout << Multiply(5, 8) << std::endl;
	std::cin.get();
}
  • inline 的作用就是获取实际的函数体,并将函数调用替换为函数体,比如在 Log.cpp
# include "Log.h"

void InitLog()
{
	// Log("Initialized Log");
	std::cout << "Initialized Log" << std::endl;
}




004 变量和函数

  • 当我们创建一个变量的时候,他们会被放到内存(堆和栈)

  • 不同变量类型之间的唯一区别:在内存中所占大小,而不同类型的变量所占内存大小与编译器有关

  • int 数据类型可以存储 -20亿 ~ +20 亿之间(约数)的值

  • 变量的类型大小与它能存储多大的数字直接相关

  • unsigned 数据类型取值范围 0 ~ 40 亿,是因为它把符号位移除了

# include <iostream>

int main()
{
	float variable = 4.4f;  // 在值的后面添加 f 区分 float 和 double 
	double var = 9.9;
	std::cout << variable << std::endl;
	std::cout << var << std::endl;
	std::cin.get();
}
  • bool 数据类型的取值为 true 或 false,其在计算机中表示为 1 或 0,但是该类型的变量在内存中,占 1Byte ,虽然其只是 0 或 1,只用 1bit 就行,但是在处理寻址内存时,不能寻址只有 1bit 位的内容,只能寻址字节

  • 可以使用 sizeof() 查看变量所占内存

Main.cpp

# include <iostream>

int main()
{
	bool var = false;
	std::cout << sizeof(var) << std::endl;  // 1
	std::cin.get();
}
  • 函数,不是 c++ 类里的

  • main() 是一个特殊的函数,可以不写函数函数返回值,但是其他的所有函数,都需要写返回值,否则会报错

  • 函数声明通常存储在头文件中,在翻译单元或 cpp 文件实现定义





005 c++ 头文件

  • 对于 c++ 而言,头文件通常用于声明某些类型的函数

  • 函数完美地解决了重复代码的问题,而头文件完美解决了函数调用必须有声明的问题,有了头文件,就不用在每个翻译单元声明函数了

  • 如果我们在 .h 头文件里包含了 #include 那么在翻译单元里,只要包含了 .h 文件,不添加 #include 也可以

  • pragma 是一个被发送到编译器或预处理器的预处理指令

  • pragma once 监督头文件,阻止单个头文件多次被包含

Log.h

# pragma once  // 只包括这个头文件一次
  • 如果我们要包含的文件,在其中的一个文件夹里,用尖括号 <> 告诉编译器,搜索包含路径文件夹。说白了就是,用引号可以指定相对路径

  • 而引号 “” 通常是告诉编译器,包含相对于当前文件的文件

  • c++ 标准库,通常没有后缀 .h,是为了和 c 进行区分

  • 在 vs 里,可以右键打开文档,查看所选头文件完整内容;右键函数名,可以查看函数定义





006 调试

  • 断点:程序中调试器将中断的点

  • 程序中断后,内存数据实际上还在

  • vs 中,F9 可以在当前行设置断点

  • step into:进入当前代码的函数里(如果有函数的话

  • step over:从当前函数跳到下一行代码

  • step out:跳出当前函数,回到调用这个函数的位置

  • 定义变量,还没初始化的时候,会随机分配一块内存空间,此时该内存空间的值就是变量的临时直

  • 在 vs 中可以在 Debug --> windows --> memory 中查看所有内存

  • 某块内存用十六进制的 cc 填充,意味是未初始化的栈内存





007 指针

  • 大道至简,指针是一个存储内存地址的整数

  • 可以使用 & 运算符配合指针,获取一个变量的内存地址(where)

Example1.cpp

# include <iostream>
# define LOG(x) std::cout << x << std::endl;

int main()
{
	void* prt = 0;  // 0 不是一个有效的内存地址,意味是无效指针
	void* prt = NULL;
	void* prt = nullptr;

	int var = 9;
	int* p = &var;  // 取 var 的地址
	*p = 10; // 修改 p 指向的内存地址的变量的值
	LOG(var);  // 输出 10,因为上一行
	std::cin.get();
}
  • 牢记:& – 取地址; * – 取内容

  • 使用数组申请的内存空间,使用完要记得释放,使用 delete 完成

  • 指针本身也是变量,它们也在内存中存储,从而演变出了双指针、三指针

Example2.cpp

# include <iostream>
# define LOG(x) std::cout << x << std::endl;

int main()
{
	char* buf = new char[8];  // 指向分配了 8 个字节的内存的开始的位置
	// 用指定的数据填充一个内存块
	memset(buf, 0, 8);  // 申请一块连续的内存空间,大小为 8 字节,使用 0 填充
	   
	char** ptr = &buf;
	   
	delete[] buf;
	std::cin.get();
}




008 c++ 引用

  • 引用必须引用已经存在的变量,引用本身不是新的变量,因此它们不占用内存

  • 引用其实是给变量起别名

  • 引用变量的主要用途是用作函数的形参,将引用变量用作参数,函数使用原始数据,而不是其副本

Example1.cpp

# include <iostream>
# define LOG(x) std::cout << x << std::endl;

int main()
{
	int a = 5;
	int& ref = a;  // a 和 ref 指向相同的值和内存单元,就是 a == ref,修改 a 和 ref 都会改变他们指向内存的变量的值
	ref = 2;
	LOG(a);  // 输出 2
	std::cin.get();
}
  • 如果在不使用全局变量的情况下,想要改变变量的值,可以使用指针和引用的方式完成

  • 使用指针的方式

Example2.cpp

# include <iostream>
# define LOG(x) std::cout << x << std::endl;

void Increment(int* value)  // 等价 int* value = &a;
{
	// 这里还要考虑运算符优先级的问题,优先级 ++ 大于 *
	(*value) ++;  // 这里要用 * ,否则 value++ 的是在地址 a 基础上,内存移位
}

int main()
{
	int a = 5;
	Increment(&a);  // 把 a 的地址作为实参
	LOG(a);  // 输出 6
	std::cin.get();
}
  • 使用引用的方式,更干净、简洁

Example3.cpp

# include <iostream>
# define LOG(x) std::cout << x << std::endl;

void Increment(int& value)
{
	value ++;
}

int main()
{
	int a = 5;
	Increment(a); 
	LOG(a);  // 输出 6
	std::cin.get();
}
  • Example2.cpp 和 Example3.cpp 的效果完全一样,引用的方法更简洁

  • 引用其实就是给变量起了一个别名,虽然它不占用内存,但是它可以修改变量的值,所以它和指针的作用一样,但是不占用内存,都可以修改变量的值

Example4.cpp

# include <iostream>
# define LOG(x) std::cout << x << std::endl;

int main()
{
	int a = 4;
	int b = 9;
	int& ref = a;
	ref = b;
	
	LOG(a);  // 输出 9
	LOG(b);  // 输出 9
	std::cin.get();
}
  • 注意:
    不能只声明引用,不赋初值
    不存在空引用,引用必须连接到一块合法的内存
    一旦引用被初始化为一个对象,就不能被指向到另一个对象。指针可以在任何时候指向到另一个对象

Example5.cpp

# include <iostream>
# define LOG(x) std::cout << x << std::endl;

int main()
{
	int a = 4;
	int b = 9;
	int& ref = a;
	ref = b;
	
	LOG(a);  // 输出 9
	LOG(b);  // 输出 9
	std::cin.get();
}
  • 通过 Example6,可以更好的理解引用

Example6.cpp

# include <iostream>
# define LOG(x) std::cout << x << std::endl;

int main()
{
	int a = 4;
	int b = 9;
	int c = 10;
	int& ref = a; 
	
	ref = b;
	LOG(b);  // 输出 9
	LOG(a);  // 输出 9
	LOG(ref);  // 输出 9
	
	ref = c;
	
	LOG(a);  // 输出 10
	LOG(b);  // 输出 9
	LOG(c);  // 输出 10
	std::cin.get();
}
  • 改变引用的值,只会改变初始化时候的变量,因为他们是指向同一块内存空间,而后续的赋值,不会改变引用原始指向的内存空间,直接看Example7 更好理解

Example7.cpp

# include <iostream>

using namespace std;

int main()
{
	int a = 4;
	
	int& ref = a;
		
	// a:4,ref:4
	cout << "a:" << a;
	cout << ",ref:" << ref << endl;
		
		
	cout << "a's add:" << &a << endl;  // a's add:0x7ffd04879624
	cout << "ref's add:" << &ref << endl;  // ref's add:0x7ffd04879624
		
	
	int b = 9;
	cout << "b's add:" << &b << endl;  // b's add:0x7ffd04879620
	ref = b;

	// b:9, ref:9, a:9
	cout << "b:" << b;
	cout << ", ref:" << ref;
	cout << ", a:" << a << endl;
		
	cout << "a's add:" << &a << endl;  // a's add:0x7ffd04879624
	cout << "ref's add:" << &ref << endl;  // ref's add:0x7ffd04879624
	cout << "b's add:" << &b << endl;  // b's add:0x7ffd04879620

	return 0;
}
  • 如果我们想要改变中间变量的值,比如 Example7 中的 b 的值,可以使用指针进行修改




009 c++ 类与结构体

  • 类只是对数据和功能组合在一起的一种方法

  • 类就是将一类物体进行抽象,然后把它们的属性封装起来

  • 由类构成的变量称为对象,新的对象变量称为实例,类中的函数称为方法

  • 由类创建的对象,就是实例(比如 main 中)

  • 默认情况下,类中所有东西都是私有的,只有类中的函数才能访问这些变量

  • 类与结构体的唯一区别:类中的所有东西是私有的,结构体中的变量是公共的

  • 结构体在 c++ 中存在的唯一原因:c 中没有类,有了结构体可以和 c 保持向后兼容

  • 如果只是向用来表示一些变量,使用 struct 是可以的,而如果有一个大量功能或继承的需求时,一般都会使用 class

Example1.cpp

# include <iostream>

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

// 类的格式
class Player
{
public:
	int x, y;
	int speed;
};

// player 在 X 轴 和 Y 轴移动的距离
void Move(Player& player, int xa, int ya)
{
	player.x += xa * player.speed;
	player.y += ya * player.speed;
}

int main()
{
	Player player;	
		
	Move(player, 1, -1);

	std::cin.get();
}
  • 下面写一个例子,介绍类的使用

Example2.cpp

# include <iostream>

class Log
{ 
public:
	const int LogLevelError = 0;
	const int LogLevelWarning = 1;
	const int LogLevelInfo = 2;
	
private :
	int m_LogLevel;
	
public:
	void SetLevel(int level)
	{
		m_LogLevel = level;
	}
	
	void Error(const char* message)
	{
		if (m_LogLevel >= LogLevelError)
			std::cout << "[ERROR]:" << message << std::endl;
	}
	
	void Warn(const char* message)
	{
		if (m_LogLevel >= LogLevelWarning)
			std::cout << "[WARNING]:" << message << std::endl;
	}
	
	void Info(const char* message)
	{
		if (m_LogLevel >= LogLevelInfo)
			std::cout << "[INFO]:" << message << std::endl;
	}
};

int main()
{
    Log log;
	log.SetLevel(log.LogLevelWarning);
	
	log.Error("Hello!");
	log.Warn("Hello!");
	log.Info("Hello!");
	
    return 0;
}




010 c++ static

  • static 在 c++ 中有两个意思,取决于上下文,一种是在类或结构体外部使用,另一种是在类或结构体内部使用

  • 类外的 static 链接将只是在内部,意味着它只能对你定义它的翻译单元可见

  • 类内的静态变量,意味着该变量实际上将与类的所有实例共享内存,这意味着该静态变量在类中创建的所有实例中,静态变量只有一个实例,就是改变一个值,其他值也会随着改变

Main.cpp

#include <iostream>

using namespace std;

// int var = 4;  // 如果 Static.cpp 中的 var 是 static 变量,则编译和链接都不会报错,否则在链接的时候,会产生变量重复的错误

// 外部链接
extern int var = 4;  // 如果 Static.cpp 中的 var 不是 static 变量,但是使用了 extern 关键字,是可以链接成功的;但是如果 Static.cpp 中的 var 是 static 变量,那么会链接时,会出错(因为 static 将 var 变量限制为私有变量了)

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

Static.cpp

static int var = 9;

Example1.cpp

#include <iostream>

using namespace std;

struct Entity 
{
	int x, y;
	
	void Print()
	{
		cout << x << " " << y << endl;
	}
};


int main()
{
	Entity e;
	e.x = 2;
	e.y = 3;
	
	Entity e1 = {4, 9};	
	
	e.Print();  // 2 3
	e1.Print();  // 4 9 
	
    return 0;
}
  • static 如果和变量一起使用,意味着在类的所有实例中,这个变量只有一个实例,这句话什么意思呢?就是在 Example2 里,不管我们创建了几个 Entity 实例,他们都只有一个实例,也就是他们所有的实例都共享一块内存

  • static 修饰的变量不再是类成员,就像我们在名为 Entity 的命名空间中,创建了两个变量,但是它们实际上并不属于类

  • 可以 Example1.cpp 和 Example2.cpp 对比理解

Example2.cpp

#include <iostream>

using namespace std;

struct Entity 
{
	static int x, y;
	
	void Print()
	{
		cout << x << " " << y << endl;
	}
};

// 如果要使用 static 变量,需要在类外进行再次
int Entity::x;
int Entity::y;

int main()
{
	Entity e;
	e.x = 2;
	e.y = 3;
	
	// Entity e1 = {4, 9};  // 如果是 static 修饰的 Entity 变量,不能直接赋值,因为 x 和 y 不是类成员
	Entity e1;
	Entity::x = 4;
	Entity::y = 9;
	// e1.x = 4;  // 其实这样初始化是错误的
	// e1.y = 9;
	e.Print();  // 2 3
	e1.Print();  // 4 9 
	
    return 0;
}
  • 静态方法不能访问非静态变量

  • 如果我们在 static void Print() 中不传入 Entity 的参数,是会报错的,因为静态方法没有办法访问 Entity 的 x 和 y 变量

Example3.cpp

#include <iostream>

using namespace std;

struct Entity 
{
	int x, y;
	
	static void Print(Entity e)
	{
		cout << e.x << " " << e.y << endl;
	}
};

// static void Print(Entity e)  // 不可以访问类中的私有成员
// {
// 	cout << e.x << " " << e.y << endl;
// }

int main()
{
	Entity e;
	e.x = 2;
	e.y = 3;
	
	Entity e1;
	e1.x = 4;
	e1.y = 9;
	
	e.Print(e);  // 2 3
	e1.Print(e1);  // 4 9
	
    return 0;
}
  • 变量的作用域:可以访问变量的范围,如果在一个函数中声明一个变量,那么 我们就不能在其他函数中访问它,因为我们声明的变量是局部的

  • 静态局部(local static)变量的生存周期基本上相当于整个程序的生存期,但是它的作用范围被限制在函数内

  • 函数作用域中的 static 和类作用域中的 static 之间没有太大的区别,因为生存周期实际上是相同的,唯一的区别是,在类作用域中,类中的任何东西都可以访问它(这个静态变量),但是如果你在函数作用域中声明一个静态变量,那么它将是函数的局部变量(这里局部变量指的是,只有当前翻译翻译才能使用它,而全局变量是在所有的 cpp 文件里都可以调用它)

Example4.cpp

#include <iostream>
using namespace std;

void Function()
{
	static int i = 0;  // 声明为 static 变量,也相当于在方法外声明的全局变量
	
	i++;
	
	// 如果 i 不是静态变量,不管 main() 调用 Function 函数多少次,最终的输出结果都是 1
	cout << i << endl;  
	
}


int main()
{
    
	Function();  // 1
	Function();  // 2
	Function();  // 3
	Function();  // 4
	
    return 0;
}
  • 再解释一下变量的生存周期,如果是静态变量,生存周期就是永远

  • 下面介绍一种单例的牛波一方法

Example5.cpp

#include <iostream>
using namespace std;

class Singleton
{
public:
	static Singleton& Get() 
	{ 
		static Singleton s_Instance;  // 这里没声明为静态变量,就会在栈上创建,代码运行到这个函数结束时,变量就会被销毁
		return s_Instance; 
	}
	
	void Hello() { }
};

int main()
{
	Singleton::Get().Hello();
	
    return 0;
}

持续更新ing…

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值