因为要学习计算机视觉,首先需要学习C++。这是因为很多图库都依赖于C++,无法绕过它。从我个人的角度来看,以JAVA和Python为开发语言,学习抽象的内容是最具性价比的选择。抽象是一种将复杂问题简化为更容易理解和解决的方法。通过学习抽象,我们可以更好地理解计算机视觉的核心概念和算法。此外,还可以学习如何使用图库和工具来实现计算机视觉应用程序。因此,在学习计算机视觉之前,掌握C++以及抽象的基本概念是非常重要的。
之所以要学习计算机视觉,是因为它在许多应用中发挥着重要的作用。为了更好地理解和应用计算机视觉,我们首先需要学习C++编程语言。C++是一种强大且广泛使用的编程语言,许多图库和工具都是使用C++开发的。因此,学习C++将帮助我们更好地理解和使用这些图库,为计算机视觉的学习打下坚实的基础。
头文件在C和C++编程中非常重要,它们有以下作用:
头文件用于包含函数和数据结构的声明,以便源代码文件可以引用它们。例如,#include
表示引用了APR库的头文件,使得源代码可以使用APR提供的函数和数据结构而不必了解它们的实现细节。同样,#include "rpc.h"
表示引用了RPC库的自定义头文件,以便在项目中使用RPC相关的功能。头文件在项目中的作用是提供接口和声明,使得代码更加模块化和可维护。
学习使用 C++ 构建工具是非常重要的,特别是当我们的程序变得越来越复杂时。通过掌握这些工具,我们能够更好地组织和管理我们的代码,提高开发效率。此外,使用构建工具还可以帮助我们自动化构建过程,减少出错的可能性,提高软件质量。因此,在学习 C++ 的过程中,我们不可避免地需要学习和掌握各种构建工具,这将对我们的编程能力和项目开发带来巨大的帮助。
当我们讨论头文件和库的用法时,让我们考虑一个简单的例子:一个程序要计算两个数的和。我们将使用头文件和库来实现这个功能。
首先,我们将创建一个名为addition.h的头文件,其中包含一个函数的声明,该函数将计算两个整数的和:
source 代码
// Created by 谢嘉伟 on 2023/12/21.
//
// addition.c
#include "addition.h"
int add(int a, int b) {
return a + b;`
}
头文件
//
// Created by 谢嘉伟 on 2023/12/21.
//
#ifndef LEARN_C_ADDITION_H
#define LEARN_C_ADDITION_H
/**
* @brief Adds two integers.
*
* This function takes two integers as input and returns their sum.
*
* @param a The first integer.
* @param b The second integer.
* @return The sum of the two integers.
*/
int add(int a, int b);
#endif //LEARN_C_ADDITION_H
main程序代码
#include
// main.c
#include "addition.h"
int main() {
int num1 = 5;
int num2 = 7;
int result = add(num1, num2);
printf("The sum of %d and %d is %d\\\\n", num1, num2, result);
return 0;
}
cmake内容
# 要求最低的CMake版本为3.27
cmake_minimum_required(VERSION 3.27)
# 定义一个名为learn_c的项目
project(learn_c)
# 设置C++的使用标准是C++ 17
set(CMAKE_C_STANDARD 17)
# 把名为main.cpp、addition.h 和 addition.cpp的文件编译成一个名为learn_c的可执行文件
add_executable(learn_c main.cpp
addition.h
addition.cpp)
最后,我们可以使用CMake工具来构建这个项目。假设您已经在项目目录下,可以执行以下命令:
mkdir build cd build cmake .. make
./addition_example
由 cmake 生成的 make内容会很多:
# Makefile
# Compiler and flags
CC = gcc
CFLAGS = -std=c99
# Source files
SRCS = main.c addition.c
# Object files
OBJS = $(SRCS:.c=.o)
# Executable name
EXE = addition_example
all: $(EXE)
$(EXE): $(OBJS)
$(CC) $(CFLAGS) -o $@ $^
%.o: %.c
$(CC) $(CFLAGS) -c $<
clean:
rm -f $(EXE) $(OBJS)`
在这个 Makefile 中,我们定义了以下内容:
CC:C编译器。
CFLAGS:编译器标志,包括C标准。
SRCS:源文件列表。
OBJS:目标文件列表。
EXE:可执行文件名。
然后,我们使用规则来构建项目:
all:默认目标,构建可执行文件。
( E X E ) :生成可执行文件的规则,依赖于目标文件( (EXE):生成可执行文件的规则,依赖于目标文件( (EXE):生成可执行文件的规则,依赖于目标文件((OBJS))。
%.o: %.c:生成目标文件的规则,将每个 .c 文件编译成 .o 文件。
要构建项目,只需在项目目录中执行 make 命令:
请注意,使用 make 自行编写 Makefile 可以更灵活地管理构建过程,但也需要更多的手动工作和维护。 CMake 可以自动生成跨平台的构建配置,使构建过程更简单和可移植。
这些都是在 Makefile
中使用的特殊变量和语法,用于定义构建规则和命令。让我解释它们的含义:
$<
:这是一个自动变量,表示规则中的第一个依赖项(通常是源文件)。在规则中,$<
用于引用规则中的第一个依赖项。
o $@ $^
:这是一个构建命令,用于指定如何将目标文件($@
)从依赖项($^
)编译。具体解释如下:
o
:这是 gcc
编译器的选项,用于指定输出文件的名称。$@
:这是一个自动变量,表示规则中的目标文件(通常是可执行文件或目标文件)。$^
:这是一个自动变量,表示规则中的所有依赖项(通常是源文件和其他目标文件)。因此,-o $@ $^
的意思是将所有依赖项编译链接到目标文件($@
)中,并指定输出文件的名称。
$(EXE)
:这是一个Makefile变量,代表可执行文件的名称。在您的示例中,$(EXE)
是 addition_example
。
$(CC)
和 $(CFLAGS)
:这些是Makefile变量,通常用于定义编译器和编译器选项。
$(CC)
:表示C编译器的名称。在您的示例中,它是 gcc
。$(CFLAGS)
:表示编译器的选项,通常包括编译标准、警告选项、优化等。在您的示例中,std=c99
是指定C标准为C99的编译选项。这些变量和语法使得Makefile能够更加灵活地管理和自动化构建过程,从而简化了项目的维护和构建。
int number = 42;
int* ptr = &number; // 声明一个指向整数的指针,将其指向number的地址
float floatNumber = 3.14;
float* ptrFloat = &floatNumber; // 声明一个指向浮点数的指针,将其指向floatNumber的地址
char character = 'A';
char* ptrChar = &character; // 声明一个指向字符的指针,将其指向character的地址
int arr[5] = {1, 2, 3, 4, 5};
int* ptrArr = arr; // 声明一个指向整数数组的指针,不需要使用&来获取数组的地址
int* dynamicPtr = new int; // 动态分配一个整数的内存,并将指针指向它
*dynamicPtr = 100; // 使用指针访问分配的内存
delete dynamicPtr; // 释放动态分配的内存,防止内存泄漏
class MyClass {
public:
void print() {
std::cout << "Hello from MyClass!" << std::endl;
}
};
MyClass obj;
MyClass* ptrObj = &obj; // 声明一个指向MyClass对象的指针
ptrObj->print(); // 使用指针访问对象的成员函数
int add(int a, int b) {
return a + b;
}
int (*funcPtr)(int, int) = add; // 声明一个指向函数的指针,将其指向add函数
int result = funcPtr(3, 4); // 使用指针调用函数
这些示例涵盖了指针在C++中的不同用途,包括指向基本数据类型、数组、动态内存、对象和函数的指针。指针是C++中强大的概念,允许您更灵活地操作内存和数据。
template <typename T>
T add(T a, T b) {
return a + b;
}
int result1 = add(3, 4); // 调用模板函数add
double result2 = add(2.5, 3.7); // 调用模板函数add
template <typename T>
class MyStack {
private:
std::vector<T> elements;
public:
void push(const T& element) {
elements.push_back(element);
}
T pop() {
if (!elements.empty()) {
T top = elements.back();
elements.pop_back();
return top;
} else {
throw std::runtime_error("Stack is empty");
}
}
};
MyStack<int> intStack; // 创建一个整数类型的栈
MyStack<double> doubleStack; // 创建一个双精度浮点数类型的栈
template <>
bool isEqual<std::string>(const std::string& a, const std::string& b) {
return a.compare(b) == 0;
}
bool result = isEqual("hello", "world"); // 调用特化的isEqual
template <typename T>
class Base {
public:
T data;
};
template <typename T>
class Derived : public Base<T> {
public:
void printData() {
std::cout << this->data << std::endl;
}
};
Derived<int> derivedObj;
derivedObj.data = 42;
derivedObj.printData(); // 输出 42
template <int N>
struct Factorial {
static const int value = N * Factorial<N - 1>::value;
};
template <>
struct Factorial<0> {
static const int value = 1;
};
int result = Factorial<5>::value; // 计算5的阶乘
这些示例涵盖了C++中模板的不同用途,包括函数模板、类模板、特化、继承和模板元编程。模板是C++中强大的特性,可以用于创建通用和高度可重用的代码。
函数模板特化是一种在模板编程中的特殊用法,用于提供对特定数据类型的定制化实现。它与普通函数和普通函数模板有一些区别和用途:
template T add(T a, T b)
,用于加法操作。但是,对于某些特定数据类型,您想要提供不同的实现。在这种情况下,您可以使用函数模板特化来覆盖通用模板的实现。 template <typename T>
T add(T a, T b) {
return a + b;
}
// 函数模板特化,为字符串类型提供不同的实现
template <>
std::string add(std::string a, std::string b) {
return a + " " + b;
}
这里,函数模板特化允许您为字符串类型提供自定义的加法实现,而通用模板仍然适用于其他数据类型。
为什么不直接使用函数而使用函数模板或函数模板特化的原因在于,模板提供了更高的通用性和灵活性。它允许您编写一次代码,然后根据不同的数据类型生成多个函数,从而避免了代码的重复编写。函数模板特化则为您提供了在某些情况下需要特殊处理的机会,同时仍然保留了通用性。
总之,函数模板和函数模板特化是你中实现通用和定制化代码的重要工具,可以提高代码的可维护性和可扩展性。
new
分配动态数组 : int size = 5; // 数组大小
int* dynamicArray = new int[size]; // 动态分配一个整数数组
// 初始化数组元素
for (int i = 0; i < size; ++i) {
dynamicArray[i] = i * 2;
}
// 使用数组
for (int i = 0; i < size; ++i) {
std::cout << dynamicArray[i] << " ";
}
// 释放动态分配的内存
delete[] dynamicArray;
new
分配对象 : class MyClass {
public:
MyClass(int value) : data(value) {}
void print() {
std::cout << "Value: " << data << std::endl;
}
private:
int data;
};
MyClass* dynamicObj = new MyClass(42); // 动态分配一个MyClass对象
// 使用对象
dynamicObj->print();
// 释放动态分配的对象内存
delete dynamicObj;
std::vector
:std::vector
是C++标准库中的容器,它在内部使用动态内存分配来管理可变大小的数组。 #include
std::vector<int> dynamicVector; // 使用std::vector动态创建一个整数数组
// 添加元素到向量
for (int i = 0; i < 5; ++i) {
dynamicVector.push_back(i);
}
// 使用向量
for (int element : dynamicVector) {
std::cout << element << " ";
}
std::shared_ptr
和 std::unique_ptr
)可以自动管理动态分配的内存,防止内存泄漏。 #include
std::shared_ptr<int> dynamicInt = std::make_shared<int>(10); // 使用std::shared_ptr动态创建整数
// 使用智能指针
std::cout << "Value: " << *dynamicInt << std::endl;
这些示例演示了C++中如何使用动态内存分配来创建动态数组、对象、容器以及如何使用智能指针来管理动态内存。动态内存分配允许程序在运行时灵活地管理内存,但也需要谨慎地释放内存,以避免内存泄漏。
std::vector
容器 :std::vector
是一个动态数组,可以根据需要自动调整大小。 #include
std::vector<int> numbers; // 声明一个整数向量
// 向向量添加元素
numbers.push_back(1);
numbers.push_back(2);
numbers.push_back(3);
// 遍历向量
for (int num : numbers) {
std::cout << num << " ";
}
std::map
容器 :std::map
是一个关联容器,用于存储键-值对。 #include
#include
std::map<std::string, int> studentScores; // 声明一个学生分数的映射
// 添加学生分数
studentScores["Alice"] = 95;
studentScores["Bob"] = 88;
studentScores["Charlie"] = 92;
// 查找学生分数
int score = studentScores["Bob"];
std::algorithm
算法 : #include
#include
std::vector<int> numbers = {5, 2, 8, 1, 9};
// 对向量进行排序
std::sort(numbers.begin(), numbers.end());
// 查找向量中的元素
int target = 8;
bool found = std::binary_search(numbers.begin(), numbers.end(), target);
std::string
类 :std::string
类是C++中用于处理字符串的常用工具。 #include
std::string greeting = "Hello, world!";
// 获取字符串长度
std::size_t length = greeting.length();
// 查找子字符串
std::size_t pos = greeting.find("world");
std::queue
和 std::stack
容器适配器 :std::queue
和 std::stack
提供了队列和栈的功能。 #include
#include
std::queue<int> q; // 声明一个队列
std::stack<int> s; // 声明一个栈
// 在队列中添加元素
q.push(1);
q.push(2);
// 在栈中添加元素
s.push(3);
s.push(4);
这些示例演示了C++ STL的一些常见用法,包括容器(std::vector
、std::map
)、算法(std::sort
、std::binary_search
)、字符串处理(std::string
)、容器适配器(std::queue
、std::stack
)等。STL使得C++编程更加方便和高效。
#include
指令用于包含其他头文件,允许您在当前文件中使用其声明。 #include // 包含标准库头文件
#include "myheader.h" // 包含用户定义的头文件
#define
指令用于创建宏定义,可以替换源代码中的文本。 #define PI 3.14159265359
#define SQUARE(x) (x * x)
#ifdef
、#ifndef
、#endif
等指令用于条件编译,根据条件选择性地包含代码。 #ifdef DEBUG
// 调试代码
#endif
#include
指令也可以用于条件包含头文件。 #ifdef USE_FEATURE
#include "feature.h"
#endif
#undef
指令用于取消已定义的宏。 #undef PI
#
运算符用于将宏参数转换为字符串。 #define STRINGIZE(x) #x
std::cout << STRINGIZE(Hello) << std::endl; // 输出 "Hello"
##
运算符用于将宏参数连接在一起。 #define CONCAT(x, y) x##y
int xy = CONCAT(10, 20); // 编译器将其解释为 int xy = 1020;
#assert
指令用于在编译时进行条件断言。 #define DEBUG
#ifdef DEBUG
#assert MAX_SIZE > 0
#endif
这些是C++预处理器的一些常见用法示例。预处理器指令用于在编译之前执行文本替换和条件编译,允许您根据需要自定义编译过程。请注意,预处理器指令通常以 #
开头,不属于C++的正式语法,而是在编译前处理的。
头文件,并使用 std::thread
类创建和管理线程。 #include
#include
void threadFunction() {
std::cout << "Hello from a thread!" << std::endl;
}
int main() {
// 创建并启动一个新线程
std::thread t(threadFunction);
// 主线程继续执行
std::cout << "Hello from the main thread!" << std::endl;
// 等待新线程完成
t.join();
return 0;
}
#include
#include
void printMessage(const std::string& message) {
std::cout << message << std::endl;
}
int main() {
std::string msg = "Hello from a thread!";
// 通过lambda表达式传递参数给线程
std::thread t([&msg]() {
printMessage(msg);
});
t.join();
return 0;
}
#include
#include
#include
// 计算一段范围内的和
void calculateSum(int start, int end, int& result) {
result = 0;
for (int i = start; i <= end; ++i) {
result += i;
}
}
int main() {
int numThreads = 4;
int rangeStart = 1;
int rangeEnd = 100;
int totalSum = 0;
std::vector<std::thread> threads;
for (int i = 0; i < numThreads; ++i) {
int start = rangeStart + (i * (rangeEnd - rangeStart + 1) / numThreads);
int end = rangeStart + ((i + 1) * (rangeEnd - rangeStart + 1) / numThreads) - 1;
threads.push_back(std::thread(calculateSum, start, end, std::ref(totalSum)));
}
for (std::thread& t : threads) {
t.join();
}
std::cout << "Total Sum: " << totalSum << std::endl;
return 0;
}
std::mutex
)来保护多线程访问共享数据的情况。 #include
#include
#include
std::mutex mtx;
int sharedData = 0;
void incrementSharedData() {
for (int i = 0; i < 1000000; ++i) {
std::lock_guard<std::mutex> lock(mtx);
sharedData++;
}
}
int main() {
std::thread t1(incrementSharedData);
std::thread t2(incrementSharedData);
t1.join();
t2.join();
std::cout << "Shared Data: " << sharedData << std::endl;
return 0;
}
这些示例演示了C++多线程编程的不同方面,包括创建和启动线程、传递参数给线程、多线程计算以及使用互斥锁来保护共享数据。多线程编程在并发和并行应用程序中非常有用,可以充分利用多核处理器的性能。
#include
#include
// 信号处理函数
void signalHandler(int signum) {
std::cout << "Received signal: " << signum << std::endl;
}
int main() {
// 注册信号处理函数
signal(SIGINT, signalHandler); // 捕获Ctrl+C中断信号
std::cout << "Running... Press Ctrl+C to interrupt." << std::endl;
while (true) {
// 程序主循环
}
return 0;
}
在上面的示例中,我们使用 signal
函数来注册信号处理函数 signalHandler
,该函数在接收到信号时被调用。在本例中,我们捕获了 SIGINT
信号,该信号是由Ctrl+C键触发的。当用户按下Ctrl+C时,程序会执行信号处理函数,然后继续执行。
请注意,信号处理函数应该尽量简单,不要执行复杂的操作,因为它们在不同的上下文中执行,并且可能中断正在执行的代码。此外,一些信号是不可捕获或不应该被自定义处理的,因此请小心选择要处理的信号。
C++还提供了一些其他与信号处理相关的函数和工具,例如 sigaction
函数允许更复杂的信号处理设置,而 kill
函数用于向其他进程发送信号。信号处理是处理操作系统级事件的重要方式之一,但在多线程应用程序中需要格外小心,以避免潜在的并发问题。