本篇主要在于记录一下我学CPP的过程,内容基于自己的理解。
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(); // 等待用户输入,防止程序结束
}
#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有没有重复引用的方法
为什么有有时候用
include<stdio.h>
而有时候是
include"Log.h"
因为引号”” 可用于相对位置的头文件,而 <>却只能用于 某一些 头文件,这些头文件都可以用引号表示.
扩展名问题
include<iostream>
include<stdio.h>
**C++**都没有扩展名,C却大多有扩展名,这也是区分标准库属于哪的判断依据.
当我们有一个条件指向一个分支的时候,我们就是在告诉我们的电脑,嘿,快跳到这个内存地址,执行这个指令。
许多高效的代码可能避免分支结构,可能内存中指令和分支距离较远时候会带来效率问题。
#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).
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可以直接结束循环,需要返回值。
#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++。
int main()
{
int i = 0;
while (i < 10)
{
if (i == 5) // 当 i 等于 5 时退出循环
{
break;
}
printf("i = %d\n", i);
i++;
}
只要调节为真,就立马跳出循环。
#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);
}
我们很容易对什么时候使用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的信息
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
表示内存是这个类中所有实体所共享的,就算是实例化了很多次这个类/结构体,那个静态变量也只会有一个实例
static
成员变量时,这个变量属于类本身,而不是任何一个对象。它在内存中只存在一次,不管有多少个对象被创建,这个静态成员变量都是共享的。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
访问,它们都是指向同一个内存位置,即这个变量在整个程序中只有一个实例。MyClass
,并创建了 100 个 MyClass
对象,如果这些对象中有一个普通的非静态成员变量 x
,那么每个对象都有自己独立的 x
,在内存中会有 100 个 x
的副本;但是,如果是一个静态成员变量 y
,那么这 100 个对象访问的都是同一个 y
,在内存中只有一个 y
实例。ByChatGPT
当我们有多个编译单元时候,在一个编译单元定义了一个变量,却没有用static
,那么我们怎么在另一个单元里调用这个变量呢?
//这是1.ccp
int Variable = 5
//这是2.cpp
extern int Variable
通过Extern
我们就可以实现在另外的编译单元找定义,也叫外部链接 External linking
不更新了,知乎找到一个博主笔记,写的比我好得多
— 2024年10月15日