C++ 知识笔记
C++
C++ 编译器
C++ 编译器是将 C++ 代码转换为机器代码的工具,通常包括多个阶段的处理,如预处理、编译、汇编、链接等。
写在源文件中的源代码是人类可读的源。它需要"编译",转为机器语言,这样 CPU 可以按给定指令执行程序。
大多数的 C++ 编译器并不在乎源文件的扩展名,但是如果您未指定扩展名,则默认使用 .cpp。
工作流程
预处理(Preprocessing)
- 预处理器负责处理以
#
开头的指令,如#include
、#define
等。 #include
会将其他头文件的内容插入到当前文件中。- 宏定义会被展开,条件编译指令(如
#if
、#ifdef
)会根据条件选择性地包含或排除代码。 - 预处理步骤的输出是一个没有预处理指令的源文件,通常被称为“纯源代码”。
编译(Compilation)
- 编译器将源代码(纯源代码)转换为汇编语言代码。
- 在这个阶段,编译器会进行语法分析,检查代码是否符合语言的语法规则。
- 编译器还会进行语义分析,确保代码的类型和结构符合语言的规则。
- 汇编语言代码是与平台相关的,但它仍然不具备执行能力。
汇编(Assembly)
- 汇编程序将汇编语言代码转换为目标机器代码(通常是
.obj
或.o
文件)。 - 这些目标文件包含机器代码,但尚未与其他代码(如库文件)进行链接。
链接(Linking)
- 链接器将一个或多个目标文件(
.obj
或.o
)与所需的库文件(例如静态库、动态库)链接在一起,生成最终的可执行文件或库文件。 - 如果程序使用了外部库或其他模块的代码,链接器会查找相应的符号并将它们合并到最终的程序中。
- 在链接过程中,链接器还会解决符号引用(例如函数调用和变量使用),并处理地址重定位。
常见的 C++ 编译器
不同平台和环境上有许多 C++ 编译器,以下是一些广泛使用的 C++ 编译器:
1、GCC (GNU Compiler Collection)
- 平台:Linux、macOS、Windows(通过 MinGW)
- 描述:GCC 是一个开源的编译器,支持多种编程语言,包括 C、C++、Fortran、Ada、Go 等。它支持多种平台,广泛应用于 Linux 系统和嵌入式开发。
- 特点:
- 开源、自由软件,支持各种操作系统和硬件架构。
- 强大的优化功能,适合开发高性能程序。
- 可通过
g++
命令进行 C++ 编译。
2、MinGW (Minimalist GNU for Windows)
- 平台:Windows
- 描述:MinGW 是 GCC 的 Windows 版本,提供了 GCC 编译器的工具链,但在 Windows 平台上可以原生生成 Windows 可执行文件。MinGW 是开发 Windows 本地应用程序的一个常用工具。
- 特点:
- 提供了一个简单的、无依赖的编译器环境,可以在 Windows 上使用 GNU 工具链。
- 可以使用 GCC 编译器进行 C++ 开发。
头文件
C++ 中的头文件是程序的重要组成部分,它们通常用于声明函数、类、宏和常量等。
头文件的主要作用是将函数原型、类定义、常量、类型定义等信息提供给源代码文件,使得代码结构更加清晰、可复用,并且能避免重复定义。
通常以 .h
、.hpp
或 .hxx
为扩展名的文件,主要用于声明函数、类、宏、常量等。源代码文件通过 #include
指令引入头文件,来访问其中定义的内容。
跟Java中的import类似,导入头文件(类)就可以使用其中定义的函数、类、宏、常量等。
头文件的作用
- 函数声明:将函数的原型告诉编译器,避免编译时出错。
- 类定义:定义类的结构和成员,方便在其他文件中使用这些类。
- 常量和宏定义:提供常量和宏的定义,以便全局使用。
- 类型定义:为类型创建别名或宏定义。
- 模块化:通过头文件的引用,可以将代码分割成多个模块,增强代码的可维护性和可重用性。
如何使用头文件
通常,头文件通过 #include
指令来包含在源代码文件中。C++ 支持两种头文件包含方式:
- 标准库头文件:例如
#include <iostream>
,标准库头文件是用尖括号包围的,编译器会在标准的系统路径中查找这些文件。 - 自定义头文件:例如
#include "MyClass.h"
,用户自定义的头文件使用双引号包围,编译器会首先在当前目录查找,如果没有找到,再在系统的标准路径中查找。
头文件的内容
头文件一般包含以下几种内容:
1) 宏定义
宏可以用来定义常量或者为复杂的表达式创建别名。
// 定义常量
#define PI 3.14159
// 定义一个求最大值的宏
#define MAX(a, b) ((a) > (b) ? (a) : (b))
2) 函数声明
在头文件中声明函数原型,告诉编译器函数的名字、返回类型和参数类型。
可以理解为在头文件中声明函数相当于Java中在接口中声明方法规范,具体实现由对应的源文件(实现类)自己实现。
// 函数声明
int add(int a, int b);
void printMessage();
函数的实现通常会在对应的源文件中提供:
// 函数实现
int add(int a, int b) {
return a + b;
}
void printMessage() {
std::cout << "Hello, world!" << std::endl;
}
3) 类定义
头文件常常包含类的声明和成员函数的声明,但实现通常放在源文件中。
// MyClass.h
#ifndef MYCLASS_H // 头文件保护
#define MYCLASS_H
class MyClass {
public:
MyClass(int x); // 构造函数声明
int getValue(); // 成员函数声明
private:
int value; // 成员变量
};
#endif
实现通常放在源文件中。
// MyClass.cpp
#include "MyClass.h"
MyClass::MyClass(int x) : value(x) {}
int MyClass::getValue() {
return value;
}
头文件保护(防止重复包含)
头文件保护是一种防止头文件被多次包含的技术。它可以避免编译时由于头文件被重复包含而导致的头文件中的内容重复定义错误。
通常通过预处理指令 #ifndef
(如果没有定义)、#define
(定义)和 #endif
(结束)来实现。这个技术被称为“包含保护”或“条件编译”。
#ifndef MYCLASS_H // 如果 MYCLASS_H 没有被定义
#define MYCLASS_H // 定义 MYCLASS_H
// 类定义和函数声明等内容
#endif // MYCLASS_H // 结束条件编译
解释:
- 第一次包含头文件:
- 当编译器遇到
#include "MyClass.h"
时,它检查MYCLASS_H
是否已经定义。 - 如果
MYCLASS_H
尚未定义(即第一次包含该头文件),则编译器会定义MYCLASS_H
并继续编译头文件中的内容。
- 当编译器遇到
- 后续包含:
- 在后续遇到同一个头文件时,编译器会检查
MYCLASS_H
是否已经定义。 - 因为在第一次包含时已经定义了
MYCLASS_H
,所以后续的#include "MyClass.h"
会跳过头文件的内容,避免重复定义。
- 在后续遇到同一个头文件时,编译器会检查
命名空间
在 C++ 中,命名空间(Namespace)是用来组织代码、避免命名冲突的机制。它允许你将标识符(如类、函数、变量等)分组到一个命名的作用域中,从而减少不同库或模块间因命名相同而导致的冲突问题。
命名空间的主要作用是为标识符提供一个限定范围,避免全局命名空间污染和不同模块或库间的命名冲突。
定义命名空间
命名空间的定义使用 namespace
关键字。你可以将多个变量、函数、类等放入同一个命名空间内,形成逻辑上的分组。
命名空间的作用范围是从它的定义开始,到文件或代码块的结束。
namespace MyNamespace {
int x = 42; // 变量
void printMessage() { // 函数
std::cout << "Hello from MyNamespace!" << std::endl;
}
class MyClass { // 类
public:
void sayHello() {
std::cout << "Hello from MyClass!" << std::endl;
}
};
}
在上面的代码中,x
、printMessage
和 MyClass
都被包含在 MyNamespace
命名空间中。
使用命名空间中的成员
要访问命名空间中的成员,可以使用 作用域运算符(::
),以 命名空间名::成员名
的方式来引用。
int main() {
// 通过作用域运算符访问命名空间中的成员
std::cout << MyNamespace::x << std::endl;
MyNamespace::printMessage();
MyNamespace::MyClass obj;
obj.sayHello();
return 0;
}
命名空间的作用
-
避免命名冲突:不同的库或模块中可能会定义相同名称的函数、变量或类。通过将这些标识符放在不同的命名空间内,可以有效地避免命名冲突。
例如,
math
库和graphics
库中可能都有一个draw
函数,使用命名空间后,它们可以通过math::draw()
和graphics::draw()
来区分。 -
代码组织:命名空间可以帮助你更好地组织代码。特别是在大型项目中,命名空间将帮助你将相关功能分组在一起,使代码结构更加清晰。
-
简化代码:可以通过
using
声明来简化对命名空间成员的访问,避免每次都写命名空间::成员
。
using
声明和 using
指令
C++ 提供了 using
声明和 using
指令来简化命名空间的使用。
using
声明:引入命名空间中的单个成员。
using MyNamespace::printMessage; // 只引入 printMessage 函数
printMessage(); // 现在可以直接使用 printMessage(),而不需要加 MyNamespace::
using
指令:引入整个命名空间中的所有成员(一般不推荐在头文件中使用,可能会导致命名冲突)。
using namespace MyNamespace; // 引入整个命名空间
printMessage(); // 现在可以直接调用 MyNamespace 中的函数或变量
注意:using namespace
在大范围(如全局范围)使用时可能会引起命名冲突,因此通常建议在函数内部或局部范围内使用它。
标准命名空间 std
C++ 标准库中的所有组件(如 iostream
、vector
、string
等)都位于 std
命名空间中。在使用标准库功能时,通常需要通过 std::
来访问,例如 std::cout
、std::vector
等。
为了简化代码,你可以使用 using namespace std;
来省略 std::
前缀(但在较大的项目中,这通常不推荐,以避免命名冲突)。
#include <iostream>
using namespace std;
int main() {
cout << "Hello, World!" << endl;
return 0;
}
匿名命名空间
C++ 中还支持 匿名命名空间(没有名字的命名空间)。匿名命名空间通常用于将内部实现细节隐藏在文件内部,防止外部访问。
namespace {
int hiddenVariable = 42;
void hiddenFunction() {
std::cout << "This is a hidden function!" << std::endl;
}
}
int main() {
std::cout << hiddenVariable << std::endl;
hiddenFunction();
return 0;
}
类
C++ 中的 类 是面向对象编程(OOP)的核心概念之一。类定义了对象的结构和行为,是对现实世界中事物的抽象。类可以包含成员变量(属性)和成员函数(方法),并且可以通过 封装、继承、多态 等特性来实现更高层次的功能。
类的基本定义
类是一个自定义数据类型,它包含成员变量和成员函数。类的定义使用 class
关键字。
类通常位于头文件中定义,而实现则在源文件中完成。
#include <iostream>
using namespace std;
// 类定义
class MyClass {
public:
int x; // 成员变量
void print() { // 成员函数
cout << "Value of x: " << x << endl;
}
static void log(const QString &log); // 静态方法
};
const
:保护参数不被修改:函数内部不能更改
log
的值。提高代码可读性:向调用者表明函数不会修改传入的值,增强代码的可维护性。
void log(QString log); // 没有 const log = "Modified"; // 函数内部可以意外地修改传入值,可能导致不可预期的行为。 static void log(const QString &log); // 不允许修改参数
&:加上&通过指针传递
在 C++ 中,
QString
是一个复杂的对象,直接按值传递会进行一次拷贝。这会导致额外的性能开销,尤其是在参数较大时。避免对象拷贝:通过引用传递,不会创建
QString
的副本。提高性能:尤其当
QString
较大时,减少内存分配和拷贝操作。void log(QString log); // 按值传递 // 每次调用 log(),会拷贝一份 QString 对象。 static void log(const QString &log); // 按引用传递 // 只传递对象的引用,避免了拷贝,性能更高。
类的对象和实例化
类定义了一个类型,可以通过 实例化 创建类的对象。对象是类的实例,每个对象都有自己独立的成员变量。
int main() {
MyClass obj; // 创建 MyClass 类型的对象 obj
obj.x = 10; // 设置成员变量 x 的值
obj.print(); // 调用成员函数 print
return 0;
}
输出
Value of x: 10
Test test;
- 创建方式:这是一种栈上创建对象的方式。
- 生命周期:
test
是一个局部对象,它的生命周期由其作用域决定。也就是说,它会在所在的代码块(如函数)结束时自动销毁。- 内存分配:
test
被分配在栈上,栈空间是由编译器自动管理的,不需要手动释放内存。- 内存效率:栈上的对象创建和销毁非常快速,不需要额外的内存分配和释放操作。
Test *test = new Test;
- 创建方式:这是在堆上动态创建一个对象。
- 生命周期:
test
是一个指针,指向堆上的Test
对象。堆上的对象的生命周期由程序员手动控制,必须显式调用delete
来销毁对象,否则会发生内存泄漏。- 内存分配:
new
运算符会在堆上分配内存,并返回该内存的地址,赋给test
指针。堆内存的分配和释放较栈内存慢,且需要程序员管理内存(手动释放delete防止内存泄漏
)。- 内存效率:堆内存的分配和管理较为复杂,通常比栈内存要慢,并且容易引起内存泄漏(如果忘记
delete
)。如何选择:
特性 Test test;
Test *test = new Test;
创建位置 栈上 堆上 生命周期 在作用域结束时自动销毁 需要手动管理,直到调用 delete
才会销毁内存管理 自动管理 需要程序员手动释放内存( delete
)内存分配开销 低,快速 较高,分配和销毁堆内存比栈内存慢 指针类型 直接是对象 是一个指向对象的指针
- 栈上对象(
Test test;
)通常用于局部变量,不需要在函数外部使用或动态控制生命周期的场景。栈内存管理简便,适合大多数情况下的对象创建。- 堆上对象(
Test *test = new Test;
)适用于需要在函数外部使用对象,或对象的生命周期不与当前作用域绑定的情况。堆对象适合动态创建、存储较大的数据结构,或需要跨函数、跨线程传递的对象。
void func() {
Test test; // 创建一个栈上对象
// 使用 test
} // 作用域结束,test 对象自动销毁
void func() {
Test *test = new Test; // 在堆上创建对象
// 使用 test
delete test; // 手动销毁对象,释放堆内存
}
构造函数和析构函数
- 构造函数:用于在创建对象时初始化对象的状态。构造函数与类同名,不返回任何类型(即使是
void
)。 - 析构函数:用于在对象销毁时释放资源或执行清理操作。析构函数的名称与类同名,前面加上
~
符号。
class MyClass {
public:
int x;
// 构造函数
MyClass(int val) {
x = val;
cout << "Constructor called, x = " << x << endl;
}
// 析构函数
~MyClass() {
cout << "Destructor called, x = " << x << endl;
}
};
int main() {
MyClass obj(10); // 创建对象并调用构造函数
// 当 obj 离开作用域时,会自动调用析构函数
return 0;
}
初始化列表
用于在构造函数体执行前初始化成员变量或调用基类构造函数。
调用基类的构造函数。
调用基类的构造函数可以初始化基类的成员变量
- 在 C++ 中,当一个类继承另一个类时,派生类会包含基类的成员变量和状态。
- 这些基类成员变量通常需要通过基类的构造函数来初始化。
- 如果不显式调用基类构造函数,编译器会尝试调用基类的默认构造函数(如果存在)。但是,如果基类没有默认构造函数(例如基类只有带参数的构造函数),则必须显式调用。
初始化成员变量。
成员变量的访问修饰符
C++ 提供了三种访问修饰符,用于控制成员变量和成员函数的访问权限:
- public:公有的,任何地方都可以访问。
- private:私有的,只能在类的成员函数中访问,外部无法直接访问。
- protected:受保护的,子类可以访问,外部无法直接访问。
class MyClass {
public:
int publicVar; // 公有成员
private:
int privateVar; // 私有成员
};
int main() {
MyClass obj;
obj.publicVar = 10; // 可以访问 public 成员
// obj.privateVar = 20; // 错误!无法访问 private 成员
return 0;
}
类的继承
继承是面向对象编程的一个重要特性,允许一个类从另一个类派生出来,并继承其成员和方法。C++ 支持单继承和多继承。
class Base {
public:
void baseMethod() {
cout << "Base class method" << endl;
}
};
class Derived : public Base { // Derived 继承自 Base
public:
void derivedMethod() {
cout << "Derived class method" << endl;
}
};
int main() {
Derived obj;
obj.baseMethod(); // 继承自 Base 类
obj.derivedMethod(); // 来自 Derived 类
return 0;
}
虚函数
在 C++ 中,虚函数(virtual function)是用于实现多态的关键机制。它允许在继承体系中通过基类指针或引用调用派生类中的重写版本,而不是基类版本,从而实现动态绑定(dynamic binding)。
虚函数是基类中声明为 virtual
的成员函数,旨在让派生类重写该函数,从而实现多态性。虚函数的特点是,它会根据对象的实际类型(而非指针或引用的类型)来决定调用哪个版本的函数。
class Base {
public:
virtual void speak() { // 基类的虚函数
std::cout << "Base speaking" << std::endl;
}
};
class Derived : public Base {
public:
void speak() override { // 派生类重写基类的虚函数
std::cout << "Derived speaking" << std::endl;
}
};
virtual
:在基类中声明一个虚函数。override
:在派生类中重写基类的虚函数时,建议使用override
关键字,它是编译器提供的一种安全机制,帮助检查是否正确重写了基类的虚函数。
虚函数的运行时行为(动态绑定)
虚函数的多态性通过运行时动态绑定实现。当通过基类指针或引用调用虚函数时,实际调用的函数是派生类中重写的版本,而不是基类的版本。这个决定是在程序运行时做出的,而不是编译时。
int main() {
Base* basePtr = new Derived(); // 基类指针指向派生类对象
basePtr->speak(); // 调用的是派生类的 speak(),而不是基类的
delete basePtr;
return 0;
}
指针
指针 是 C++ 中非常重要的概念,它允许程序员直接操作内存地址。通过指针,你可以访问和操作内存中的数据,动态分配内存,以及实现函数间的复杂数据传递。
指针是一个变量,它存储另一个变量的内存地址。换句话说,指针指向内存中的某个位置,存储该位置的数据。
指针的声明
指针的声明语法是:类型 *指针名
,这里的 类型 是指针指向的变量的类型。
int x = 10;
int *p = &x; // p 是一个指向 int 类型变量的指针,存储了变量 x 的地址
&x
是取x
变量的地址,返回的是x
的内存地址。p
是一个指向int
类型的指针,存储了x
的地址。
解引用操作符
解引用操作符(*
)用于通过指针访问或修改指针所指向的值。
int x = 10;
int *p = &x; // p 存储了 x 的地址
cout << *p << endl; // 使用 *p 获取 p 指向的值,即 x 的值,输出 10
*p = 20; // 通过指针修改 x 的值
cout << x << endl; // 输出 20
指针的常见操作
指针偏移:指针可以通过加法或减法进行偏移,从而访问不同内存地址。
int arr[] = {10, 20, 30};
int *p = arr;
cout << *(p + 1) << endl; // 访问 arr[1],输出 20
指针比较:可以比较指针的大小,判断它们是否指向相同的地址。
int a = 10, b = 20;
int *p1 = &a;
int *p2 = &b;
if (p1 != p2) {
cout << "Pointers are not equal" << endl;
}
指针的空值和 nullptr
指针可以没有指向任何有效的内存地址,这时指针的值是 空指针,在 C++11 以后,nullptr
是标准的空指针常量。
int *p = nullptr; // p 是一个空指针,指向无效内存地址
int *p = NULL; // 老旧的 C++ 代码中使用 NULL
指针的类型
指针的类型决定了指针可以指向什么类型的数据。例如,int *p
是一个指向 int
类型数据的指针,double *p
是一个指向 double
类型数据的指针。
int a = 10;
double b = 5.5;
int *p1 = &a; // p1 是一个指向 int 类型的指针
double *p2 = &b; // p2 是一个指向 double 类型的指针
指针与数组
在 C++ 中,数组名本身就是指向数组第一个元素的指针。可以通过指针来遍历数组。
int arr[] = {1, 2, 3, 4, 5};
int *p = arr; // p 指向 arr 的第一个元素
for (int i = 0; i < 5; i++) {
cout << *(p + i) << " "; // p + i 是指针偏移,解引用得到数组元素
}
注意: arr
和 &arr[0]
是相同的,都是指向数组第一个元素的指针。
指针与函数
指针可以作为函数的参数,用于实现 按地址传递。这使得函数可以修改传入变量的值。
void increment(int *p) {
(*p)++; // 修改 p 指向的值
}
int main() {
int x = 5;
increment(&x); // 传递 x 的地址
cout << x << endl; // 输出 6
return 0;
}
使用 new
和 delete
int *p = new int; // 分配一个整数大小的内存
*p = 10; // 赋值
cout << *p << endl; // 输出 10
delete p; // 释放内存
// --------------------------------------------
int *arr = new int[5]; // 分配一个整数数组
for (int i = 0; i < 5; i++) {
arr[i] = i + 1;
}
delete[] arr; // 释放动态数组
指针作为返回值
指针也可以作为函数的返回类型,从而返回一个指向数据的地址。
int* createArray() {
int *arr = new int[5]; // 动态分配内存
for (int i = 0; i < 5; i++) {
arr[i] = i + 1;
}
return arr; // 返回指向动态分配数组的指针
}
int main() {
int *p = createArray();
for (int i = 0; i < 5; i++) {
cout << p[i] << " "; // 输出 1 2 3 4 5
}
delete[] p; // 释放动态分配的内存
return 0;
}
p[i]
是指针运算,等同于*(p + i)
,通过偏移p
来访问数组的不同元素
总结
指针是 C++ 中一个非常强大且灵活的工具,它允许你直接操作内存、动态分配内存和实现复杂的数据传递。通过指针,你可以:
- 直接访问和修改内存地址中的数据。
- 实现动态内存分配和释放。
- 通过函数传递地址,修改外部变量。
- 操作复杂的数据结构(如数组、链表等)。
虽然指针带来了很大的灵活性,但也需要谨慎使用,因为不当使用指针(如野指针、内存泄漏等)可能导致程序崩溃或不可预测的行为。因此,使用指针时需要特别注意内存管理和指针的生命周期。
引用
引用是一个指向对象的别名,它允许在函数中直接操作传递给它的原始对象,而不是对象的副本。
为什么使用引用?
使用引用有几个好处:
- 避免复制:如果
PDW_CPP
类型的对象非常大,传递引用可以避免复制整个对象,提高效率。 - 可以修改原始对象:引用允许函数直接修改传递给它的对象。如果你传递的是引用,函数对该参数所做的修改会影响到调用该函数时传入的对象。
QList<DiscernResult> PDWDiscernNormal(PDW_CPP &pdws, int pdwlen, DatAndPls* datAndPls, const QList<PDW_Origin>& pdwData, const ConfigINI_CPP& config);
PDW_CPP &pdws
:表示函数PDWDiscernNormal
的第一个参数是一个对PDW_CPP
类型的 引用。这里的
&
不是取地址符号,而是声明引用的语法。在函数签名中,&
表示pdws
是引用类型。
作用域运算符
在 C++ 中,作用域运算符 ::
是一个双冒号符号,用于指示特定的作用域。它通常用于限定名称,以便区分全局作用域、命名空间作用域、类作用域等。作用域运算符在许多场景下非常重要,特别是在代码复杂且命名冲突较多时。
全局作用域
访问全局作用域中的变量或函数
#include <iostream>
using namespace std;
int x = 10; // 全局变量
int main() {
int x = 20; // 局部变量
cout << "局部变量 x: " << x << endl; // 输出 20
cout << "全局变量 x: " << ::x << endl; // 输出 10
return 0;
}
x
是局部变量。::x
明确表示访问全局变量x
。
命名空间作用域
C++ 使用命名空间来组织代码,避免命名冲突。作用域运算符用于指定要访问的命名空间中的成员。
#include <iostream>
namespace MyNamespace {
int x = 42;
}
int main() {
cout << "命名空间中的变量: " << MyNamespace::x << endl;
return 0;
}
类或结构作用域
作用域运算符可以用于限定类或结构中的静态成员、嵌套类型或枚举值。
(1) 访问静态成员
#include <iostream>
using namespace std;
class MyClass {
public:
static int x; // 静态成员变量
};
int MyClass::x = 100; // 定义静态成员变量
int main() {
cout << "静态成员变量: " << MyClass::x << endl;
return 0;
}
(2)访问基类的成员
在继承关系中,当派生类的成员覆盖了基类的成员时,可以使用作用域运算符 ::
来明确调用基类的成员。
#include <iostream>
using namespace std;
class Base {
public:
void show() {
cout << "Base 类的 show 函数" << endl;
}
};
class Derived : public Base {
public:
void show() {
cout << "Derived 类的 show 函数" << endl;
}
};
int main() {
Derived obj;
obj.show(); // 调用派生类的函数
obj.Base::show(); // 调用基类的函数 明确调用基类 Base 的 show 函数。
return 0;
}
(3)定义类的成员函数(类外定义)
作用域运算符用于类外定义成员函数,以表明该函数属于某个特定的类。
#include <iostream>
using namespace std;
class MyClass {
public:
void display(); // 函数声明
};
// 类外定义函数
void MyClass::display() {
cout << "类的成员函数" << endl;
}
int main() {
MyClass obj;
obj.display();
return 0;
}
访问枚举类
如果枚举类型的名字与其他作用域中的名字发生冲突,可以使用作用域运算符来区分。
#include <iostream>
using namespace std;
enum Color { Red, Green, Blue };
int main() {
cout << "Red 的值是: " << Color::Red << endl;
return 0;
}
数据结构
线性数据结构是数据元素按顺序排列的结构,每个元素有且仅有一个前驱和后继。常见的线性数据结构包括:
数组(Array)
- 定义:数组是一种固定大小、同类型元素的顺序集合。元素可以通过索引访问。
- 特点:快速随机访问,连续内存存储,大小固定(除非使用动态数组)。
- 缺点:插入和删除元素时效率低,特别是在中间操作时。
int arr[5] = {1, 2, 3, 4, 5};
动态数组(std::vector
)
std::vector
是 C++ 标准库提供的一个动态数组容器,它支持动态大小,并且自动管理内存。vector
是通过 堆内存分配的,可以在运行时调整大小,自动扩展。
std::vector
是一个动态数组,能够根据需要自动扩展大小。vector
提供了许多内建的方法来简化操作,支持随机访问和高效的尾部插入。- 内存管理:
vector
会自动调整容量以容纳新元素,而无需程序员手动管理内存。
#include <iostream>
#include <vector>
int main() {
// 创建一个空的 vector
std::vector<int> vec;
// 向 vector 中添加元素
vec.push_back(1);
vec.push_back(2);
vec.push_back(3);
// 访问和输出 vector 中的元素
for (int i = 0; i < vec.size(); i++) {
std::cout << vec[i] << " ";
}
std::cout << std::endl;
// 使用范围-based for 循环
for (int value : vec) {
std::cout << value << " ";
}
return 0;
}
链表(Linked List)
- 定义:链表是一种由节点组成的数据结构,每个节点包含数据部分和指向下一个节点的指针。根据链接方向,可以分为单向链表、双向链表、循环链表等。
- 特点:支持高效的插入和删除操作,尤其是在链表的头部或中间进行插入时。
- 缺点:无法进行高效的随机访问,节点需要额外的内存空间来存储指针。
struct Node {
int data;
Node* next;
};
结构体
在 C++ 中,结构体(struct
) 是一种用户定义的数据类型,用于将不同类型的变量组合在一起。结构体常用于表示多个属性相关的数据项。结构体的成员可以是不同的数据类型,包括基本数据类型、数组、指针、甚至其他结构体。
#include <iostream>
#include <string>
// 定义一个表示学生信息的结构体
struct Student {
std::string name; // 学生姓名
int age; // 学生年龄
float grade; // 学生成绩
};
int main() {
// 创建结构体变量
Student student1;
// 赋值
student1.name = "Alice";
student1.age = 20;
student1.grade = 90.5f;
// 打印结构体成员
std::cout << "Name: " << student1.name << std::endl;
std::cout << "Age: " << student1.age << std::endl;
std::cout << "Grade: " << student1.grade << std::endl;
return 0;
}
结构体的对比:struct
vs class
- 访问控制:
struct
默认所有成员都是public
,而class
默认所有成员是private
。这意味着在struct
中,你可以直接访问成员变量,而在class
中需要通过访问函数(getter/setter)来访问成员。- 功能差异:
struct
和class
都支持成员函数、继承、多态等特性,但struct
更常用于简单的数据存储,而class
通常用于更加复杂的对象建模。
结构体数组
struct PDW_Origin { float RF; // 4字节 double PW; // 8字节 }; // 通过 new[] 操作符为 pdw_Origin 分配一块新的内存,大小为 pdwTotal 个 PDW_Origin 类型的对象。 // 这意味着 pdw_Origin 现在指向一个 PDW_Origin 类型的动态数组。 PDW_Origin *pdw_Origin=new PDW_Origin[pdwTotal]; // 分配新的内存,总字节大小:pdwTotal * 12
结构体大小
struct Struct29S { unsigned char space1_1; // 1 unsigned char space1_2; unsigned char space1_3; unsigned char space1_4; int pw;// 4 long long TOA; // 8 int pa; float RF;// 4 unsigned char space2_1; unsigned char space2_2; unsigned char space2_3; unsigned char space2_4; unsigned int datastart; unsigned int dataend; // 4 }; sizeof(Struct29S) // 40
4 (space1_1 to space1_4) + 4 (pw) + 8 (TOA) + 4 (pa) + 4 (RF) + 4 (space2_1 to space2_4) + 8 (datastart, dataend)
= 36 字节
字节填充了4字节
数据类型
整数
long long
long long
是 C++ 中的一种整数类型,通常表示 64 位(即 8 字节)的带符号整数。- 它的值范围从 -2^63 到 2^63 - 1(即大约从 -9.2×10^18 到 9.2×10^18)。
- 它通常用于需要存储大范围整数的场景,比如时间戳、文件大小等。
宏
在 C++ 中,宏(Macro)是通过 预处理器(Preprocessor)定义的一种文本替换机制。宏在代码编译之前由预处理器处理,它们可以是常量值、代码片段或者条件编译指令等。宏通常用于提高代码的可维护性、减少重复代码以及为不同的平台提供条件编译支持。
在 C++ 中,宏是通过 #define
指令来定义的。#define
会定义一个名称(或常量),并将其映射到特定的文本内容或代码。
常量宏
常量宏用于定义常量值。
#define PI 3.14159
在这个例子中,PI
会被替换为 3.14159
,任何地方使用 PI
的地方都会被预处理器替换成 3.14159
。
函数宏
函数宏是带有参数的宏,可以用于将一段代码(通常是计算公式)替换到目标位置。
#define SQUARE(x) ((x) * (x))
这里,SQUARE(x)
是一个宏,它会将 x
替换为 (x * x)
。例如,SQUARE(3)
会被替换成 ((3) * (3))
,从而实现了计算平方的功能。
注意:
- 宏在替换时只是文本替换,所以要小心括号的使用,避免出现意外的运算顺序问题。
- 宏展开时不会进行类型检查,所以有时可能会导致一些难以追踪的错误。
Lambda
Lambda 表达式是 C++11 引入的一种强大的功能,它允许你在函数内部定义匿名函数,并能够直接在调用位置使用。Lambda 表达式非常灵活,可以用来简化回调、事件处理等场景中的代码。
Lambda 表达式的基本语法
[捕获列表] (参数列表) -> 返回类型 { 函数体 }
各个部分解释:
捕获列表
[ ]
:用来捕获外部变量到 Lambda 表达式中。
- 捕获外部变量以供 Lambda 表达式使用。你可以捕获具体的变量或者捕获所有变量。
参数列表
( )
:类似于普通函数的参数列表,指定 Lambda 表达式的输入参数。
- 如果不需要参数,可以留空:
[](){}
。返回类型
-> 返回类型
:指定 Lambda 表达式的返回类型。如果可以推断返回类型,可以省略。函数体
{ }
:Lambda 表达式的具体实现,包含你希望执行的代码。
带参数的 Lambda
Lambda 表达式可以接收参数,类似于普通函数。
auto add = [](int a, int b) -> int {
return a + b;
};
std::cout << add(10, 20) << std::endl; // 输出 30
指定返回类型
Lambda 可以指定返回类型(如果返回类型无法自动推断)。如果 Lambda 返回类型是 void
或其他类型,可以使用 ->
显式指定。
auto multiply = [](int a, int b) -> int {
return a * b;
};
std::cout << multiply(3, 4) << std::endl; // 输出 12
库函数
memcpy
memcpy
是一个标准的 C 库函数,用来从源地址复制指定字节数的数据到目标地址。 具体参数说明:
float rfCenter = 0; memcpy(&rfCenter, byData.data() + 104, 4);
&rfCenter
:目标地址,指向rfCenter
变量的内存位置。memcpy
会将数据复制到这个地址。
byData.data() + 104
:源地址,指向byData
中的第 104 个字节。byData
是一个QByteArray
(或者类似的容器),.data()
返回该容器的底层数据指针,然后加上 104,表示从第 104 个字节开始读取数据。
4
:复制的字节数。由于rfCenter
是一个float
类型,float
类型通常占用 4 个字节,因此这里指定复制 4 字节。将
byData
从第 104 字节开始的 4 个字节复制到rfCenter
变量中。然后,rfCenter
将保存这 4 个字节所表示的float
类型的数值。
QVector<QCPGraphData> *mData2 = nullptr; memcpy(mData2->data() + 1, mData->data() + 1, sizeof(QCPGraphData) * 2);mData2->data() + 1:目标地址偏移,表示从
mData2
的第 1个元素开始写入。mData->data() + 1:源地址偏移,表示从
mData
的第 1个元素开始读取。sizeof(QCPGraphData) * 2:要拷贝的总字节数,计算方式:
单个 QCPGraphData 的大小 × 元素个数
。( sizeof(QCPGraphData) 就是数组每个元素的字节大小,*2表示拷贝2个元素)
假设:
mData
存储{A, B, C, D, E}
(size=5
)。
startPosX = 1
,nCount = 2
(要拷贝B, C
)。
mData2Pointer = 1
(从mData2
的第 1 个位置开始写入)。
mData2
存储{?, ?, ? ,?, ?}执行后:
mData2
的内容变为{?, B, C, ?, ?}
(?
表示原有数据)。
memset
memset 是 C/C++ 标准库中的一个函数,用于将一段内存区域填充为指定的值。
函数原型:void* memset(void* ptr, int value, size_t num);
ptr:指向要填充的内存区域的指针。
value:要填充的值(以 int 形式传递,但实际填充时会被转换为 unsigned char)。
num:要填充的字节数。
UDP_Data udp_data;
memset(&udp_data, 0, sizeof(udp_data));
// 初始化结构体 UDP_Data 的所有成员为 0, sizeof(udp_data)获取结构体的大小
size_t nLastPos[4] = {0};
std::memset(nLastPos, 0, sizeof(nLastPos));
// nLastPos 是目标数组的地址,0 是要设置的值,sizeof(nLastPos) 计算数组的字节大小。
// 该调用将 nLastPos 数组的所有字节设置为 0,即将数组的所有元素设置为 0。