侵权删
相关岗位需求
学习网站
C++
C++自救指南
零声
经典面试题
音视频开发
学习资料汇总
嵌入式八股
学习资料
1.Makefile
Makefile 是一个包含了一组指令的文件,这些指令用于自动化编译和构建软件项目的过程。它是 make
工具的配置文件,make
是一个广泛使用的工具,旨在简化和管理编译过程。Makefile 的主要作用和特点包括:
1.1. 自动化构建
Makefile 可以自动执行编译整个项目的一系列命令,这包括编译源代码文件、链接生成可执行文件以及其他构建相关的任务。
1.2. 依赖性管理
Makefile 可以定义文件之间的依赖关系。例如,它可以指定一个对象文件依赖于特定的源代码文件。如果源代码文件被修改了,make
会重新编译这个对象文件以及依赖于它的任何其他文件。
1.3. 避免不必要的重编译
make
工具通过检查文件的时间戳来决定是否需要重新编译文件。只有当依赖文件(如源文件)的时间戳比目标文件(如对象文件)更新时,make
才会执行编译命令。这可以显著减少构建时间。
1.4. 模块化构建
在大型项目中,Makefile 允许项目被划分为多个较小的模块,每个模块都可以有自己的 Makefile。这样可以更好地管理复杂项目的构建过程。
1.5. 定制命令
Makefile 允许开发者自定义编译和链接命令,以及执行其他任何构建前后需要执行的脚本或命令。这包括清理项目、生成文档、打包软件等。
1.6. 参数化构建
Makefile 可以包含可配置的变量,使得构建过程可以根据不同的环境或配置参数而有所不同。例如,可以有针对调试和发布的不同编译选项。
7. 广泛应用
Makefile 不仅用于 C/C++ 项目,还可以用于其他语言和类型的项目,如 Java、Python 或任何需要自动化步骤的项目。
总的来说,Makefile 是一个强大的工具,可以帮助软件开发者和工程师自动化和简化编译过程,提高效率,确保构建的一致性和可重复性。
2.Linux
之前对Linux的接触就是搭建深度学习环境的服务器了,一直听同学说在Linux下的视频编解码速度会快点儿,所以现在也记录一下自己学习Linux的一些入门笔记。
主要优势
-
开源性:
- Linux 是一个开源操作系统,这意味着其源代码是可公开获取的,用户和开发者可以自由地修改和分发代码。这种开放性促进了创新和快速的问题解决。
-
成本效益:
- 大多数 Linux 发行版可以免费获取和使用,这对于预算有限的个人和企业来说是非常有吸引力的。相比之下,Windows 许可证通常需要付费,尤其是在企业环境中。
-
安全性:
- Linux 通常被认为比 Windows 更安全。它的权限和用户角色分离做得更加严格,加上有活跃的社区持续监控和修补安全漏洞。此外,Linux 的用户群较小,使得它不太成为恶意软件的主要目标。
-
稳定性和可靠性:
- Linux 系统以其高稳定性和可靠性而闻名,非常适合长时间运行的服务器和系统。Linux 系统不需要经常重启来恢复性能,而 Windows 系统更新后通常需要重启。
-
灵活性和定制性:
- Linux 提供了极高的定制性,用户可以控制几乎所有的系统方面,包括使用不同的图形用户界面或完全无界面。此外,用户可以选择只安装他们需要的软件和服务,从而优化系统性能和资源使用。
-
多平台支持:
- Linux 能够运行在各种硬件上,从个人电脑、Mac 电脑到超级计算机甚至是嵌入式系统,如树莓派。这使得 Linux 在不同的技术领域都有广泛的应用。
-
命令行接口(CLI):
- Linux 的命令行接口是一个功能强大的工具,对开发者尤其有用。它允许用户快速执行复杂的任务,而无需图形用户界面。
-
支持开源软件:
- Linux 与广泛的开源软件生态系统紧密集成,为用户提供了丰富的软件选择,这些软件大多数是免费可用的。
-
社区支持:
- Linux 拥有一个非常活跃和支持性强的社区。无论是通过论坛、IRC 频道还是邮件列表,用户都可以获得免费的支持和建议。
-
适合编程和开发:
- Linux 提供了多种编程语言和工具的原生支持。它是开发者的首选环境,特别是对于服务器和数据库管理、网站和软件开发等领域。
常见版本
-
Ubuntu
- Ubuntu 是最受欢迎的 Linux 发行版之一,以其用户友好性和广泛的社区支持而闻名。它是基于 Debian 的,提供了稳定的桌面、服务器和云计算版本。Ubuntu 的用户界面整洁,安装和配置过程简单,非常适合 Linux 新手。
- ubuntu清华镜像文件下载 企业用18.04较多
-
Debian
- Debian 是一个非常稳定的系统,以其庞大的软件仓库和严格的软件包管理策略而著称。它是许多其他 Linux 发行版的基础,包括 Ubuntu 和 Linux Mint。Debian 适合那些寻求稳定性和安全性的高级用户和开发者。
-
Fedora
- Fedora 是一个创新型的操作系统,专注于集成最新的自由和开源软件。它与 Red Hat Enterprise Linux(RHEL)紧密相关,并经常作为企业级产品的测试平台。Fedora 对于那些希望体验最新技术的用户来说是理想的选择。
-
CentOS(红帽就是这个的企业版)
- CentOS(社区企业操作系统)是 RHEL 的一个免费版本,完全兼容 RHEL 的二进制。它主要用于服务器和在商业环境中非常受欢迎,因为它提供了企业级的稳定性而无需额外的许可费用。2021年,CentOS 的开发方向发生了变化,推出了 CentOS Stream,作为 RHEL 的上游(即测试和开发版)。
-
Arch Linux
- Arch Linux 是为高级用户设计的,它提倡 KISS 原则(“保持简单,愚蠢”)。它的特点是滚动更新和用户完全控制安装过程。Arch 也以其广泛的文档和社区支持而闻名,适合那些愿意深入学习 Linux 工作方式的用户。
-
Linux Mint
- Linux Mint 是基于 Ubuntu 的另一个非常用户友好的发行版,特别注重提供一个完整的“开箱即用”的体验,包括所有必要的插件、编解码器和支持软件。它的界面和菜单布局让从 Windows 过渡来的用户感觉更为自然和容易上手。
-
openSUSE
- openSUSE 是一个德国发起的项目,提供两种版本:Tumbleweed(滚动更新)和 Leap(定期发布)。它以其易于配置的 YaST 管理工具和高度的可配置性而受到好评。openSUSE 适合开发者和专业用户。
VMware
VSCODE
学习笔记
常量指针 指针常量
在C++中,const
关键字用来定义常量值,即这些值一旦初始化后就不可被改变。关于const
在指针中的使用,确实有点复杂,因为它可以放在不同的位置以影响指针和指针指向的数据。
-
const int* p1 = &a;
- 这里,
const
修饰的是int
,意味着p1
是一个指向const int
的指针。你不能通过p1
修改它指向的值,即*p1
是不可修改的。但是,你可以改变p1
本身的值,即你可以让p1
指向另一个整数。 - 示例:
*p1 = 5;
// 错误,不能修改p1
指向的值 - 示例:
p1 = &b;
// 正确,可以改变指针指向
- 这里,
-
int* const p2 = &a;
- 这里,
const
修饰的是整个指针p2
,使得p2
成为一个常量指针。p2
必须始终指向被初始化的地址(这里是a
的地址),你不能改变指针的指向,但可以通过p2
来修改它所指向的值。 - 示例:
p2 = &b;
// 错误,不能改变指针本身的指向 - 示例:
*p2 = 5;
// 正确,可以修改指针指向的值
- 这里,
-
const int* const p = &a;
- 这是一个指向
const int
的常量指针。这里,第一个const
修饰int
,表示不能通过p
来修改指向的值;第二个const
修饰指针p
本身,意味着p
的指向一旦被初始化后也不能改变。 - 示例:
*p = 5;
// 错误,不能修改指向的值 - 示例:
p = &b;
// 错误,不能改变指针本身的指向
- 这是一个指向
结构体
#include<iostream>
#include<string>
#include<ctime>
using namespace std;
struct Student
{
string name;
int age;
int score;
};
struct Teacher
{
string name;
struct Student sArry[5];
};
void inPutInformation(struct Teacher tArry[], int len)
{
string Name = "ABCDE";
for (int i = 0; i < len; i++)
{
tArry[i].name = "Teacher_";
tArry[i].name += Name[i];
for (int j = 0; j < 5; j++)
{
tArry[i].sArry[j].name = "Student_";
tArry[i].sArry[j].name += Name[j];
int random = rand()% 60 +40;
tArry[i].sArry[j].score = random;
}
}
}
void printInformation(struct Teacher tArry[],int len)
{
for (int i = 0; i < len; i++)
{
cout << "老师的姓名:" << tArry[i].name << endl;
for (int j = 0; j < 5; j++)
{
cout << "\t学生的姓名:" << tArry[i].sArry[j].name << "考试分数:" << tArry[i].sArry[j].score << endl;
}
}
}
int main(void)
{
srand((unsigned int)time(NULL));
struct Teacher tArry[3];
int len = sizeof(tArry) / sizeof(tArry[0]);
inPutInformation(tArry,len);
printInformation(tArry,len);
system("pause");
return 0;
}
内存分区模型
#include <iostream>
// 全局变量,存放在全局区,程序启动时创建,程序结束时销毁
int globalVariable = 10;
// 静态变量,同样存放在全局区,程序启动时创建,程序结束时销毁
static int staticVariable = 20;
// 常量,存放在全局区的只读部分,程序启动时创建,程序结束时销毁
const int constant = 30;
// 函数,存放在代码区,程序加载时创建,程序结束时销毁
void function(int arg) {
// 局部变量,存放在栈区,函数调用时创建,函数返回时销毁
int localVariable = arg;
std::cout << "Local variable on stack: " << localVariable << std::endl;
// 动态分配的内存,存放在堆区,手动创建,手动销毁
int* heapVariable = new int(40);
std::cout << "Heap variable: " << *heapVariable << std::endl;
// 释放堆区内存,释放后销毁
delete heapVariable;
}
int main() {
std::cout << "Global variable: " << globalVariable << std::endl;
std::cout << "Static variable: " << staticVariable << std::endl;
std::cout << "Constant: " << constant << std::endl;
// 调用函数,参数传递过程中会创建副本在栈上,函数返回后这些副本销毁
function(50);
return 0;
}
前缀自增和后缀自增
前缀自增 operator++()
在前缀自增中,函数返回类型为 MyClass&
,即返回的是一个对当前对象的引用。这是因为前缀自增的目的是先增加对象的值,然后直接使用增加后的对象。通过返回引用,我们避免了不必要的对象复制,同时允许自增表达式可以作为左值使用。
MyClass& operator++()
{
++m_val; // 首先增加m_val的值
return *this; // 然后返回当前对象的引用
}
在这里,*this
是对调用该方法的对象的引用,因此这个方法直接修改对象的状态并返回修改后的对象。
后缀自增 operator++(int)
后缀自增的情况稍微复杂一些。这里的返回类型是 MyClass
而不是 MyClass&
。这意味着返回的是当前对象的一个副本,而非引用。后缀自增的行为是:首先将当前对象的状态保存到一个临时对象中,然后增加当前对象的值,最后返回刚才保存的临时对象(即自增前的状态)。
MyClass operator++(int)
{
MyClass temp = *this; // 创建当前对象的副本
++m_val; // 增加当前对象的m_val值
return temp; // 返回增加前的副本
}
在这个过程中,temp
是在自增操作之前的对象的一个副本。当你执行 b = a++;
时,a
的值首先被保存到 temp
中,然后 a
的 m_val
被增加,最后返回 temp
。这意味着 b
将接收到自增前的 a
的值。
关键差异
- 返回类型:前缀自增返回一个引用(
MyClass&
),因此更高效;后缀自增返回一个值(MyClass
),涉及到对象的复制。 - 自增操作与返回值:前缀自增返回的是增加后的对象,可以用作左值;后缀自增返回的是增加前的对象的副本,不能用作左值。
这些差异确保了每种操作符的行为符合预期,即前缀自增立即反映变化,后缀自增提供了操作前的状态。希望这能帮助你更好地理解这两种操作的实现和使用场景。
作用域
在C++中,::
是作用域解析操作符,它用于指定变量、函数、或类型名称的作用域。这个操作符非常有用,尤其是在处理多重继承、命名空间或任何可能引起名称冲突的情况下。
作用域解析操作符(::)
1. 访问全局变量
当局部变量与全局变量同名时,可以使用 ::
前缀来访问全局变量。
int value = 5; // 全局变量
void function() {
int value = 10; // 局部变量
cout << value << endl; // 输出10,局部变量
cout << ::value << endl; // 输出5,全局变量
}
2. 类作用域
如果派生类(子类)中有与基类(父类)中同名的成员,可以通过作用域解析操作符来访问基类中被遮蔽的成员。
class Base {
public:
int m_A = 1;
};
class Derived : public Base {
public:
int m_A = 2;
};
void function() {
Derived obj;
cout << obj.m_A << endl; // 输出2,访问派生类的m_A
cout << obj.Base::m_A << endl; // 输出1,访问基类的m_A
}
3. 类静态成员
可以使用类名和作用域解析操作符来访问类的静态成员,无需创建类的实例。
class MyClass {
public:
static int value;
};
int MyClass::value = 100;
void function() {
cout << MyClass::value << endl; // 访问静态成员
}
作用域概念的重要性
- 避免命名冲突:当多个标识符可能在不同的作用域中有相同的名称时,作用域解析操作符提供了一种方法来明确指定哪一个标识符被使用。
- 明确意图:它清晰地表明程序员的意图,使代码更易于理解和维护。
- 访问控制:它允许程序员在继承中明确地访问基类中的成员,即使在派生类中存在同名成员。
- 代码组织:通过命名空间或类作用域使用作用域解析操作符,可以更好地组织代码,使其结构清晰。
C++中的作用域解析操作符是一种强大的语言特性,它提供了访问和控制代码不同部分的能力,是处理复杂系统时不可或缺的工具。
static 修饰类成员变量和函数
静态成员变量 (static int count):
静态成员变量是类级别的变量,而不是对象级别的变量。它们在所有对象之间共享。
静态成员变量必须在类外进行定义和初始化,否则会导致链接错误。
静态成员函数 (static void foo()):
静态成员函数可以访问静态成员变量,但不能访问非静态成员变量和非静态成员函数(因为它们没有 this 指针)。
静态成员函数可以直接通过类名调用,而无需创建对象。
#include <iostream>
using namespace std;
class MyClass {
public:
static int count; // static 修饰类成员变量
static void foo() { // static 修饰类成员函数
cout << count << endl;
}
};
// 类外定义和初始化静态成员变量
int MyClass::count = 0;
int main() {
// 访问静态成员变量
MyClass::count = 10;
// 调用静态成员函数
MyClass::foo(); // 输出 count 的值
return 0;
}
分析 MyClass::count; 和 MyClass::foo();
MyClass::count:
这是对静态成员变量 count 的访问。因为 count 是静态的,它不属于某个特定对象,而是属于整个类。
通过 MyClass::count 可以读取或修改 count 的值。
MyClass::foo():
这是对静态成员函数 foo 的调用。静态成员函数可以通过类名直接调用,而不需要创建类的实例。
在 foo 函数中,会输出静态成员变量 count 的值。
输出
根据上述代码,以下是执行过程及输出结果:
首先在 main 函数中,将 MyClass::count 赋值为 10。
然后调用 MyClass::foo(),该函数会输出 count 的值。
因此,程序的输出将是10
为什么需要字节对齐
因为如果不对数据存储进行适当的对齐,可能会导致存取效率降低。
例如,有些平台每次读取都是从偶数地址开始。如果一个 int 类型(假设为 32 位系统)存储在偶数地址开始的位置,那么一个读周期就可以读取这 32 位。
但如果存储在奇数地址开始的位置,则需要两个读周期,并将两次读取的结果的高低字节拼凑才能得到这 32 位数据。
显然这会显著降低读取效率。
以下是字节对齐的一些基本规则:
- 自然对齐边界
对于基本数据类型,其自然对齐边界通常为其大小。
例如,char 类型的自然对齐边界为 1 字节,short 为 2 字节,int 和 float 为 4 字节,double 和 64 位指针为 8 字节。具体数值可能因编译器和平台而异。
- 结构体对齐
结构体内部的每个成员都根据其自然对齐边界进行对齐。
也就是可能在成员之间插入填充字节。
结构体本身的总大小也会根据其最大对齐边界的成员进行对齐(比如结构体成员包含的最长类型为int类型,那么整个结构体要按照4的倍数对齐),以便在数组中正确对齐。
-
联合体对齐
联合体的对齐边界取决于其最大对齐边界的成员。联合体的大小等于其最大大小的成员,因为联合体的所有成员共享相同的内存空间。 -
编译器指令
可以使用编译器指令(如 #pragma pack)更改默认的对齐规则。这个命令是全局生效的。这可以用于减小数据结构的大小,但可能会降低访问性能。 -
对齐属性
在 C++11 及更高版本中,可以使用 alignas 关键字为数据结构或变量指定对齐要求。这个命令是对某个类型或者对象生效的。例如,alignas(16) int x; 将确保 x 的地址是 16 的倍数。 -
动态内存分配
大多数内存分配函数(如 malloc 和 new)会自动分配足够对齐的内存,以满足任何数据类型的对齐要求。
举例说明下:
#include <iostream>
#pragma pack(push, 1) // 设置字节对齐为 1 字节,取消自动对齐
struct UnalignedStruct {
char a;
int b;
short c;
};
#pragma pack(pop) // 恢复默认的字节对齐设置
struct AlignedStruct {
char a; // 本来1字节,padding 3 字节
int b; // 4 字节
short c; // 本来 short 2字节,但是整体需要按照 4 字节对齐(成员对齐边界最大的是int 4)
// 所以需要padding 2
// 总共: 4 + 4 + 4
};
struct MyStruct {
double a; // 8 个字节
char b; // 本来占一个字节,但是接下来的 int 需要起始地址为4的倍数
//所以这里也会加3字节的padding
int c; // 4 个字节
// 总共: 8 + 4 + 4 = 16
};
struct MyStruct1 {
char b; // 本来1个字节 + 7个字节padding
double a; // 8 个字节
int c; // 本来 4 个字节,但是整体要按 8 字节对齐,所以 4个字节padding
// 总共: 8 + 8 + 8 = 24
};
int main() {
std::cout << "Size of unaligned struct: " << sizeof(UnalignedStruct) << std::endl;
// 输出:7
std::cout << "Size of aligned struct: " << sizeof(AlignedStruct) << std::endl;
// 输出:12,取决于编译器和平台
std::cout << "Size of aligned struct: " << sizeof(MyStruct) << std::endl;
// 输出:16,取决于编译器和平台
std::cout << "Size of aligned struct: " << sizeof(MyStruct1) << std::endl;
// 输出:24,取决于编译器和平台
return 0;
}