👻

yu

CPP笔记

本篇主要在于记录一下我学CPP的过程,内容基于自己的理解。

2024年10月15日

1.为什么用void,因为你有时候定义一个函数并不要求他要返回什么东西,只是关注于他能做什么事情。

2.为什么要有函数,因为当你想要实现某种功能的时候,你无需重复复制粘贴,只需要调用函数。

#include<iostream>

// 定义一个返回两个数乘积的函数
int Mutiply(int a, int b)
{
    return a * b;
}

// 定义 MutiplyAPP 函数,调用 Mutiply 并输出结果
void MutiplyAPP(int a, int b)
{
    int result = Mutiply(a, b); // 调用 Mutiply 计算乘积
    std::cout << result << std::endl; // 输出结果
}

int main()
{
    MutiplyAPP(4, 5); // 调用 MutiplyAPP 函数
    MutiplyAPP(6, 4);
    std::cin.get(); // 等待用户输入,防止程序结束
}
  1. 只有主函数 int main不用返回一个值,因为他会自动假设返回0.
  2. 头文件其实很有用,他的用途不只是声明,以供你在多个CPP中使用.在基础中,头文件传统上是用来声明某些函数类型,一遍用于整个函数,我们需要一个共同的地方来存放函数,只是声明,没有函数的主体。
#include<iostream>

void Log(const char* message)
{
	std::cout << message << std::endl;
}
int main()
{
}
void Log(const char* message); #这就是上面那个函数的签名

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

这就是告诉源2那个函数所在位置的方法。但是如果我们需要在别处用到呢?我们难道要复制粘贴吗?头文件在这的作用就发挥出来看了,他可以复制粘贴 函数到我们需要用这个函数的地方。

检查include有没有重复引用的方法

  1. 头文件生成时自带#pragma once
  2. 还有一种 ifentif

为什么有有时候用

include<stdio.h>

而有时候是

include"Log.h"

因为引号”” 可用于相对位置的头文件,而 <>却只能用于 某一些 头文件,这些头文件都可以用引号表示.

扩展名问题

include<iostream>
include<stdio.h>

**C++**都没有扩展名,C却大多有扩展名,这也是区分标准库属于哪的判断依据.

2024年10月17日

条件语句

当我们有一个条件指向一个分支的时候,我们就是在告诉我们的电脑,嘿,快跳到这个内存地址,执行这个指令

许多高效的代码可能避免分支结构,可能内存中指令和分支距离较远时候会带来效率问题。

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

int main()
{
	int x = 5;
	bool compare = x == 5;
	if (compare);
	{
		InitLog();
		Log("hello world");
	}
	std::cin.get();
}

再这行代码里,布尔 bool判断x是否为5,如果则打印(compare == true).

2024年10月18日

FOR循环以及WHILE循环

FORLOOP

#include<stdio.h>
int main() #打印五次
{
	for (int i = 0; i < 5; i++)
	{
		printf("hello ,world\n");
	}
}

for循环需要三个要素,分别为 初始化表达式,条件表达式(比较或者是布尔值),增量表达式。在这个循环中首先先定义变量,然后在判断是否符合条件,符合执行函数,然后在进行增量。

#include<stdio.h>
int main() //打印五次
{
	int i = 0;
	for (; i < 5; )
	{
		printf("hello ,world\n");
		i++;
	}
}

也可也把赋值放在前头,增量放在后头。

WHILELOOP

和for循环差不多,但是没有第一个初始化表达式以及第三个增量表达式。

#include<stdio.h>
int main() //打印五次
{
	int i = 0;
	while ( i < 5 )
	{
		printf("hello ,world\n");
		i++;
	}
}

DO WHILE

不同于while,即使开始条件就不满足,它也至少会执行一次。

用哪个

通常取决于是否需要新变量,选择通常看个人风格。有布尔值判断是否循环一般用WHILE。一般遍历数组时候,数字的大小确定用FOR

控制流语句

控制流语句通常和循环一起使用,这样我们就能更好的控制循环。

有三个主要的控制流语句 continue\ break \ return

continue只能用于循环内部,如果还能迭代的话,直接进行下一次迭代,否则终止循环。

break主要用在循环里,但也经常用在swich语句中。它的功能是直接跳出循环。

return可以直接结束循环,需要返回值。

continue

#include<stdio.h>
int main() //打印五次
{
	for (int i = 0; i < 5; i++)
	{
		if (i % 2 == 0)// 如果余数取值0
			continue;// 则进行下一次迭代
		printf("Hello,World \n");
	}
}

就像这样,当i为 0, 2, 4时候,不执行打印函数。直接跳到i++。

break

int main()
{
    int i = 0;
    while (i < 10)
    {
        if (i == 5) // 当 i 等于 5 时退出循环
        {
            break;
        }
        printf("i = %d\n", i);
        i++;
    }

只要调节为真,就立马跳出循环。

return

#include<stdio.h>
int main() //打印五次
{
	for (int i = 0; i < 5; i++)
	{
		if (i>2)
			return 0;
		printf("Hello,World \n");
	}
}
```c++

这是一个需要返回整数的函数,因为函数开始就已经定义了**int类型**,如果if是真,那么他会**直接关闭我们的程序**,后面代码都不会执行。

另外一点不同于上面两个函数的是,return函数不仅仅局限于函数内部,**也可以在函数外部。**直接在一个地方写个return 0就可以结束函数。 	

一定要注意如果函数需要返回一个值,那么一定要return一个值。

## 什么控制了语句

控制流语句控制了程序的运行。

这些IF语句,条件语句,循环,控制流,就是逻辑编程的核心,是用来修改程序运行情况的唯一工具。



# 2024年10月19日

## 指针

指针对于控制内存在的数据极为重要。

所有类型的指针都是一个整数,存放着一个内存地址,类型不会改变指针,指针永远只是一个地址。

```c++
void *ptr = 0 //这是一个没有类型的,无效指针,0/null 代表无意义,但可以存在。
int main
{
int var = 8;
void *ptr = &var;//取出地址
}

如果你想找到变量var在内存中存放的地址,就用**&运算符。在变量前加&实际上就是在问:hi,你的地址是什么**。

当我们知道变量var的地址,那我们怎么才能读取或改写

int main
{
int var = 8;
int *ptr = &var;
*ptr = 10;
//取出地址,int类型告诉编译器,指指针*ptr应该以整数的形式,存储这个数字
}

一个指针只指向内存中的一个位置,而不是指向一块内存。

#include<iostream>
int main()
{
	char* p = new char[8];、
	//分配8个字节,然后把这块内存开始的地址存到一个指针里。
	memset(p, 0, 8);
	//用数字0填充这8个字节
	delete[] p;
	//清理内存,防止overflow
	return 0;
}

new底层其实是call了malloc,malloc是memory allocation的简写,从名字也可以知道它负责分配内存,delete则调用了free(),区别是new和delete不仅管理内存,还会调用constructor和destructor,另外它们都是operator,所以你可以重载它们,做一些有趣的事情。对了,new【】和delete【】其实另两个operator,它们做的事情稍微有点不一样,你调用new【】的时候,必须要指定一个size,但调用delete【】的时候,并没有指定size,它怎么知道delete多少呢?这是因为new【】不仅分配了所需要的内存,还会多分配一个额外的空间,来存储这个size,所以以视频中的举例,它所做的是分配这样一块内存【8, 0, 0, 0, 0, 0, 0, 0, 0】,连续的,但是多一块在最前面,但是return给你的是跳过那块内存的地址,比如malloc返回的是0x1,但new【】给你返回的是0x1+2(我记得它分配的是一个word(一般是short)的大小,具体大小需要看系统),然后在delete【】的时候,它会往前推一个word,因为它知道前面一个word肯定是size,从而拿到size,进而delete所有)

引用

在计算机如何处理引用与指针这两个关键词上来看,他两其实是一回事。当然如何写它,如何用它还是有点不同的。

引用只是指针的一块语法糖,使得指针更易读更好学。

引用,正如其名,对于某个已存在变量的引用。对于指针可以创建一个0指正,但是引用不能这么做。因为“引用变量”必须引用一个已存在的变量,他本身并不是一个新的变量,他们并不真正的占用内存,存储数据。

	int main()
	{
		int a = 5;
		int& ref = a
		//变量类型后➕&就是指的是引用变量!
		//ref不是一个真正的变量名,它不存在于内存!
	}
}

ref他不是一个变量!他只是a的一个别称。不需要写一些奇怪的字符,只需要让它等于那个已存在的变量即可。

到目前为止我们就创建了别名,他只是为已引用对象所取的另外一个名字。

若是我们想让a值增加,通过以下函数

#include<iostream>
void Increament(int* value)
//把函数的形参变为一个 指针,这样就能把a的内存地址传入函数,而不是a本身
//传递变量的内存地址
{
	(*value)++;
//这里必须改为解引用,不然加的就是地址本身了
//由于顺序问题加括号
}
int main()
{
	int a = 5;
	int* p = &a;
	Increament(&a);
	log(a);
}

传址,如果是传值无法改变a的值,只是copy一下生成了一个 int value = 5

然而通过引用,我们可以更加简单的实现以上。

#include<iostream>
void Increament(int& value)
//这儿的int& value就是代表一个引用变量
{
	value++;
}
int main()
{
	int a = 5;
	Increament(a);
	log(a);
}

就像这样

没有什么是引用能实现,而指针不能,指针很像引用,但是它的功能更强大,而引用能让代码具有更高的可读性,更加简洁。

简而言之,就是把数据以及函数,结合在一起。

当我们想生成一个游戏,有许多玩家,这当中必定涉及许多变量,定义他们会造成混乱,难以维护和继续。

但我们可以生成一个叫做Player,一次性包含所有数据,最终作为一种类型

#include<iostream>
#define LOG(x) std :: cout << x << std :: endl
class Player
{
	int x, y;
	int speed;
};
	//创建全新的类别叫做 player
int main()
{
	Player  player;
	//这里我们创建了一个类型为Player,叫做player的对象
	//新创建对象的过程叫做实例化,这儿我们实例化了一个叫做player的对象
}

player对象无法访问类中的私有成员,因为有种东西叫做访问控制。当我创建一个类时,我可以指定属性的私有性,但是默认类中成员的访问控制都是私有的,意味着只有内部函数才能访问这些变量。

对象拥有类定义的所有属性和方法,它们是类的具体表现形式。每个对象都有自己独立的数据,但共享类的行为。

如果我们想要角色移动到某一个位置,该怎么办呢,我们可以构造一个函数。

类内可以写函数,类内的函数叫做方法。

#include<iostream>

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

class Player
{
public:
	int x, y;
	int speed;
//创建全新的类别叫做 player,并且设置为公开
void move( int xa, int ya )
{
	x += xa * speed;
	y += ya * speed;
}
//在类内添加函数,称为方法,写在里面不需要(Player& player,)
};

int main()
{
	Player  player;
//这里我们创建了一个类型为Player,叫做player的对象
//新创建对象的过程叫做实例化,这儿我们实例化了一个叫做player的对象
	player.move( 1, -1);
}

2024年10月20日

结构体和类的区别是什么

我们很容易对什么时候使用struct与class而感到困惑,它们基本没什么区别。

唯一的区别是

类默认情况下是私有的,如果我们要调用它必须有特殊的声明,而结构体默认是公有的,例如,我们可以直接把类改为结构体,这样不用public也不会报错。

#include<iostream>

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

struct Player
{
//未设置公开,改为struct
	int x, y;
	int speed;
	void move(int xa, int ya)
	{
		x += xa * speed;
		y += ya * speed;
	}};

int main()
{
	Player  player;
	player.move(1, -1);
}

这种情况下,如果我们要它变成私有,必须标识private

作用上他两没有实际上没有太大的区别,C++中结构体存在的唯一原因是,是他想要维持与C之间的兼容性,因为C没有类,然而他有结构体,如果我们突然删除struct这个关键词,那他就会失去所有的兼容性。

实际的使用场景会有所不同,因为每个人的理解都有不同,受到个人编程风格习惯的影响。

当仅仅是一些变量和操纵变量的函数是,偏向于结构体。

绝对不会对结构体使用继承,继承增加了一层复杂性,我希望我的结构体仅仅是数据的结构体。

如何写类

学会编写一个基本的Log类。

实现一下

像控制台中写入文本。错误,警告(打印警告,但不会跟踪信息),消息。

#include <iostream>
//简单的日志记录系统。我们可以通过设置日志级别来控制哪些日志信息会被打印输出。
class Log
{
public:
	const int LogLevelError = 0;
	const int LogLevelWarning = 1;
	const int LogLevelInfo = 2;
	//书写习惯,把常量和定义分开写

private:
	int m_LogLevel = LogLevelInfo;
	//m用来提示这是一个私有类的成员变量
	//用来记录当前日志记录器的日志级别,默认为 LogLevelInfo(2)

public:	
	void SetLevel(int Level)
	{
		m_LogLevel = Level;
	}
	//置日志级别,将传入的日志级别值赋给 m_LogLevel。这决定了后续哪些日志会被记录。

	void Error(const char* message)
	{
	if (m_LogLevel >= LogLevelError)
	//日志级别为 0 或更高,则输出错误信息。
		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的对象

	log.SetLevel(log.LogLevelError);
	//设置级别

	//设置一个日志级别,意味着只会打印警告或更重要的消息
	log.Warn("Hello");
	log.Error("Hello");
	log.Info("Hello");
	std::cin.get();
}

这样,当我们将级别设置为Info时候,所有信息都会被打印出来,而设置Error则只会打印出来Error的信息

关键字Static

CPP中的关键字根据上下文有两种意思。

一种是在结构体Struct或者是类Class外的,另一种则是在结构体或者类里的。

有点像类里的Private,变量和函数同理。

Static variable(静态变量)Static function(静态函数)只对它声明的CPP文件可见。

假如我们在头文件中定义了静态变量,然后在两个CPP文件中包含了这个头文件,某个变量其实在两个编译单元都被定义了,这就是静态变量在头文件中的作用,防止重复定义。

类与结构

Static如果在一个类里或者是结构里,静态意味着特定的东西。如果和变量一起使用,意味着在类的所有实例中,这个变量只有一个实例,意思是如果某个实例如果改变了这个静态变量,那么它会在所有实例中反映这个变量变化,也因此,通过类实例来引用静态变量是没有意义的

class MyClass {
public:
    static int value; // 静态成员变量
};

int MyClass::value = 10;

int main() {
    // 推荐的访问方式
    std::cout << MyClass::value << std::endl; // 直接通过类名访问

    // 不推荐的访问方式
    MyClass obj;
    std::cout << obj.value << std::endl; // 通过对象访问,但实际上它还是属于类的
    return 0;
}

通过类实例来引用静态变量是没有意义的

因为每个非静态方法总是获得当前类的一个实例作为参数,这就是类在幕后的工作方式。它们通过隐藏的参数发挥作用,静态方法不回得到那个隐藏参数

class MyClass {
public:
    int nonStaticVar;  // 非静态成员变量
    static int staticVar;  // 静态成员变量

    static void staticMethod() {
        // std::cout << nonStaticVar;  // 错误!静态方法不能访问非静态成员
        std::cout << staticVar;  // 可以访问静态成员
    }

    void nonStaticMethod() {
        std::cout << nonStaticVar;  // 可以访问非静态成员
    }
};	

int MyClass::staticVar = 10;

静态方法无法访问非静态成员的原因

类/结构体外

类外的Static修饰的符号在Link阶段是局部的,也就是他只对定义他的.Obj(编译单元)可见

类外的 static 关键字主要用于全局变量全局函数,它们的作用是限制作用域。具体特性如下:

  • 作用域:类外的 static 变量和函数仅在声明它们的文件内可见。也就是说,它们的作用域限制在定义它们的文件中,其他文件无法访问它们。
  • 生命周期:这些 static 变量在程序开始时就被创建,并在程序结束时销毁,它们的生命周期是整个程序运行期间。
  • 用途:可以防止全局命名冲突,因为静态全局变量或函数不会被其他文件看到,从而避免了不同文件中同名变量或函数的冲突。
stastic int s_Variable = 5;
//Stastic表示这个变量在link时候只对这个.cpp里的东西可见
//Link它的实际定义时候,Linker不回在这个编译单元外找它的定义

类/结构体内

而类内的stastic表示内存是这个类中所有实体所共享的,就算是实例化了很多次这个类/结构体,那个静态变量也只会有一个实例

  1. 静态成员变量的定义与特点

  • 当你在类中定义一个 static 成员变量时,这个变量属于类本身,而不是任何一个对象。它在内存中只存在一次,不管有多少个对象被创建,这个静态成员变量都是共享的。
  • 因为它是类的一个属性,所以在类的所有对象之间,它的值是共享的。如果一个对象修改了这个静态变量的值,其他对象也会看到这个变化。
  1. 具体例子
cppCopy code#include <iostream>

class Counter {
public:
    static int count; // 静态成员变量声明
    Counter() {
        count++; // 每创建一个对象,count 自增
    }
};

int Counter::count = 0; // 静态成员变量定义

int main() {
    Counter c1; // 第一个对象,count 变为 1
    Counter c2; // 第二个对象,count 变为 2
    Counter c3; // 第三个对象,count 变为 3

    std::cout << "Count: " << Counter::count << std::endl; // 输出:Count: 3
    return 0;
}

在这个例子中:

  • Counter::count 是一个静态成员变量,属于 Counter 类。
  • 每次创建一个 Counter 对象时,count 都会增加 1。因为这个 count 是静态的,所以无论创建多少个对象,它们都访问的是同一个 count 变量。
  • 最终,无论你访问哪个对象的 count 或者直接通过 Counter::count 访问,它们都是指向同一个内存位置,即这个变量在整个程序中只有一个实例
  1. 理解共享内存
  • 为什么共享?:因为静态变量不属于单个对象,而是属于整个类。所以它在类的所有对象之间都是共享的。这就像类的一个全局属性,无论创建多少个对象,它们访问的都是同一个变量。
  • 内存模型:假设我们有一个类 MyClass,并创建了 100 个 MyClass 对象,如果这些对象中有一个普通的非静态成员变量 x,那么每个对象都有自己独立的 x,在内存中会有 100 个 x 的副本;但是,如果是一个静态成员变量 y,那么这 100 个对象访问的都是同一个 y,在内存中只有一个 y 实例。
  1. 实际意义
  • 静态变量可以用于统计类对象的个数共享类级别的数据控制某些类级别的行为。比如,在单例模式中,我们通常使用静态变量来确保类只创建一个实例。

ByChatGPT

Extern

当我们有多个编译单元时候,在一个编译单元定义了一个变量,却没有用static,那么我们怎么在另一个单元里调用这个变量呢?

//这是1.ccp
int Variable = 5
//这是2.cpp
extern int Variable

通过Extern我们就可以实现在另外的编译单元找定义,也叫外部链接 External linking

2024年10月22日

不更新了,知乎找到一个博主笔记,写的比我好得多

— 2024年10月15日