学计算机视觉理论,做DEMO(第一天)

有其他语言经验的人如何学习 C++

因为要学习计算机视觉,首先需要学习C++。这是因为很多图库都依赖于C++,无法绕过它。从我个人的角度来看,以JAVA和Python为开发语言,学习抽象的内容是最具性价比的选择。抽象是一种将复杂问题简化为更容易理解和解决的方法。通过学习抽象,我们可以更好地理解计算机视觉的核心概念和算法。此外,还可以学习如何使用图库和工具来实现计算机视觉应用程序。因此,在学习计算机视觉之前,掌握C++以及抽象的基本概念是非常重要的。

为什么不仅仅学Python 而需要再学习 C++

之所以要学习计算机视觉,是因为它在许多应用中发挥着重要的作用。为了更好地理解和应用计算机视觉,我们首先需要学习C++编程语言。C++是一种强大且广泛使用的编程语言,许多图库和工具都是使用C++开发的。因此,学习C++将帮助我们更好地理解和使用这些图库,为计算机视觉的学习打下坚实的基础。

头文件在C和C++编程中非常重要,它们有以下作用:

  • 定义了函数、变量、数据结构等的声明,使得源代码文件可以引用这些声明,而无需了解实际实现细节。
  • 提供了代码的模块化和组织结构,使得不同源文件可以相互包含并共享定义。
  • 帮助编译器进行类型检查,确保代码的正确性。
  • 作为文档,提供了代码库的接口信息,让其他开发者了解如何使用库中的函数和数据结构。

头文件用于包含函数和数据结构的声明,以便源代码文件可以引用它们。例如,#include 表示引用了APR库的头文件,使得源代码可以使用APR提供的函数和数据结构而不必了解它们的实现细节。同样,#include "rpc.h" 表示引用了RPC库的自定义头文件,以便在项目中使用RPC相关的功能。头文件在项目中的作用是提供接口和声明,使得代码更加模块化和可维护。

C++构建工具 Cmake

学习使用 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中使用的特殊变量和语法,用于定义构建规则和命令。让我解释它们的含义:

  1. $<:这是一个自动变量,表示规则中的第一个依赖项(通常是源文件)。在规则中,$< 用于引用规则中的第一个依赖项。

  2. o $@ $^:这是一个构建命令,用于指定如何将目标文件($@)从依赖项($^)编译。具体解释如下:

    • o:这是 gcc编译器的选项,用于指定输出文件的名称。
    • $@:这是一个自动变量,表示规则中的目标文件(通常是可执行文件或目标文件)。
    • $^:这是一个自动变量,表示规则中的所有依赖项(通常是源文件和其他目标文件)。

    因此,-o $@ $^ 的意思是将所有依赖项编译链接到目标文件($@)中,并指定输出文件的名称。

  3. $(EXE):这是一个Makefile变量,代表可执行文件的名称。在您的示例中,$(EXE)addition_example

  4. $(CC)$(CFLAGS):这些是Makefile变量,通常用于定义编译器和编译器选项。

    • $(CC):表示C编译器的名称。在您的示例中,它是 gcc
    • $(CFLAGS):表示编译器的选项,通常包括编译标准、警告选项、优化等。在您的示例中,std=c99 是指定C标准为C99的编译选项。

这些变量和语法使得Makefile能够更加灵活地管理和自动化构建过程,从而简化了项目的维护和构建。

学习 C 重主要的差别内容:

以下是一些C++指针的常见用法示例:

  1. 指向整数的指针 :
   int number = 42;
   int* ptr = &number; // 声明一个指向整数的指针,将其指向number的地址

  1. 指向浮点数的指针 :
   float floatNumber = 3.14;
   float* ptrFloat = &floatNumber; // 声明一个指向浮点数的指针,将其指向floatNumber的地址

  1. 指向字符的指针 :
   char character = 'A';
   char* ptrChar = &character; // 声明一个指向字符的指针,将其指向character的地址

  1. 指向数组的指针 :
   int arr[5] = {1, 2, 3, 4, 5};
   int* ptrArr = arr; // 声明一个指向整数数组的指针,不需要使用&来获取数组的地址

  1. 指向动态分配的内存的指针 :
   int* dynamicPtr = new int; // 动态分配一个整数的内存,并将指针指向它
   *dynamicPtr = 100; // 使用指针访问分配的内存
   delete dynamicPtr; // 释放动态分配的内存,防止内存泄漏

  1. 指向对象的指针 (类指针):
   class MyClass {
   public:
       void print() {
           std::cout << "Hello from MyClass!" << std::endl;
       }
   };

   MyClass obj;
   MyClass* ptrObj = &obj; // 声明一个指向MyClass对象的指针
   ptrObj->print(); // 使用指针访问对象的成员函数

  1. 指向函数的指针 :
   int add(int a, int b) {
       return a + b;
   }

   int (*funcPtr)(int, int) = add; // 声明一个指向函数的指针,将其指向add函数
   int result = funcPtr(3, 4); // 使用指针调用函数

这些示例涵盖了指针在C++中的不同用途,包括指向基本数据类型、数组、动态内存、对象和函数的指针。指针是C++中强大的概念,允许您更灵活地操作内存和数据。

以下是一些C++模板的常见用法示例:

  1. 函数模板 :
   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

  1. 类模板 :
   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; // 创建一个双精度浮点数类型的栈

  1. 函数模板特化 :
   template <>
   bool isEqual<std::string>(const std::string& a, const std::string& b) {
       return a.compare(b) == 0;
   }

   bool result = isEqual("hello", "world"); // 调用特化的isEqual

  1. 模板类继承 :
   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

  1. 模板元编程 :
   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++中强大的特性,可以用于创建通用和高度可重用的代码。

函数模板特化是一种在模板编程中的特殊用法,用于提供对特定数据类型的定制化实现。它与普通函数和普通函数模板有一些区别和用途:

  1. 用途
  • 函数模板 :函数模板是一种通用性更强的代码重用方式,可以适用于多种数据类型。它允许您编写一次代码,然后将其应用于多种数据类型,从而提高了代码的灵活性和可重用性。
  • 函数模板特化 :函数模板特化用于在某些特定情况下提供特定类型的实现,以覆盖模板的通用实现。这在需要为某些类型提供不同行为的情况下非常有用。
  1. 区别
  • 函数 :普通函数是具体的代码实现,通常对特定的数据类型进行操作。每个函数都需要明确指定参数类型和返回类型。
  • 函数模板 :函数模板是一种通用的代码模板,其中参数的类型可以是任意类型,编译器根据实际参数类型生成相应的代码。
  • 函数模板特化 :函数模板特化是对函数模板的补充,用于为特定类型提供特殊的实现。它会覆盖通用函数模板的行为。
  1. 示例
    假设您编写了一个通用的模板函数 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;
   }

这里,函数模板特化允许您为字符串类型提供自定义的加法实现,而通用模板仍然适用于其他数据类型。

为什么不直接使用函数而使用函数模板或函数模板特化的原因在于,模板提供了更高的通用性和灵活性。它允许您编写一次代码,然后根据不同的数据类型生成多个函数,从而避免了代码的重复编写。函数模板特化则为您提供了在某些情况下需要特殊处理的机会,同时仍然保留了通用性。

总之,函数模板和函数模板特化是你中实现通用和定制化代码的重要工具,可以提高代码的可维护性和可扩展性。

C++中的动态内存分配允许您在运行时分配和释放内存,而不需要在编译时知道确切的内存大小。这对于处理可变大小的数据结构或动态生成的数据非常有用。以下是一些使用动态内存分配的示例:
  1. 使用 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;

  1. 使用 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;

  1. 使用 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 << " ";
   }

  1. 使用智能指针
    智能指针(如 std::shared_ptrstd::unique_ptr)可以自动管理动态分配的内存,防止内存泄漏。
   #include 

   std::shared_ptr<int> dynamicInt = std::make_shared<int>(10); // 使用std::shared_ptr动态创建整数

   // 使用智能指针
   std::cout << "Value: " << *dynamicInt << std::endl;

这些示例演示了C++中如何使用动态内存分配来创建动态数组、对象、容器以及如何使用智能指针来管理动态内存。动态内存分配允许程序在运行时灵活地管理内存,但也需要谨慎地释放内存,以避免内存泄漏。

C++标准模板库(STL)是C++的一个核心组件,提供了各种容器、算法和函数模板,用于开发高效和可维护的C++程序。以下是一些C++ STL的常见用法示例:

  1. 使用 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 << " ";
   }

  1. 使用 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"];

  1. 使用 std::algorithm 算法
    STL提供了丰富的算法,例如排序、查找、遍历等。
   #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);

  1. 使用 std::string
    std::string 类是C++中用于处理字符串的常用工具。
   #include 

   std::string greeting = "Hello, world!";

   // 获取字符串长度
   std::size_t length = greeting.length();

   // 查找子字符串
   std::size_t pos = greeting.find("world");

  1. 使用 std::queuestd::stack 容器适配器
    std::queuestd::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::vectorstd::map)、算法(std::sortstd::binary_search)、字符串处理(std::string)、容器适配器(std::queuestd::stack)等。STL使得C++编程更加方便和高效。

C++预处理器是C++编译过程中的一个重要阶段,它用于在实际的编译之前对源代码进行预处理。以下是一些C++预处理器指令的例子:
  1. 包含头文件
    #include 指令用于包含其他头文件,允许您在当前文件中使用其声明。
   #include  // 包含标准库头文件
   #include "myheader.h" // 包含用户定义的头文件

  1. 宏定义
    #define 指令用于创建宏定义,可以替换源代码中的文本。
   #define PI 3.14159265359
   #define SQUARE(x) (x * x)

  1. 条件编译
    #ifdef#ifndef#endif 等指令用于条件编译,根据条件选择性地包含代码。
   #ifdef DEBUG
   // 调试代码
   #endif

  1. 条件包含
    #include 指令也可以用于条件包含头文件。
   #ifdef USE_FEATURE
   #include "feature.h"
   #endif

  1. 取消宏定义
    #undef 指令用于取消已定义的宏。
   #undef PI

  1. 字符串化
    # 运算符用于将宏参数转换为字符串。
   #define STRINGIZE(x) #x
   std::cout << STRINGIZE(Hello) << std::endl; // 输出 "Hello"

  1. 拼接
    ## 运算符用于将宏参数连接在一起。
   #define CONCAT(x, y) x##y
   int xy = CONCAT(10, 20); // 编译器将其解释为 int xy = 1020;

  1. 条件断言
    #assert 指令用于在编译时进行条件断言。
   #define DEBUG
   #ifdef DEBUG
   #assert MAX_SIZE > 0
   #endif

这些是C++预处理器的一些常见用法示例。预处理器指令用于在编译之前执行文本替换和条件编译,允许您根据需要自定义编译过程。请注意,预处理器指令通常以 # 开头,不属于C++的正式语法,而是在编译前处理的。

以下是一些C++多线程编程的示例,使用了C++11及更高版本的标准库提供的多线程支持。要使用多线程,您需要包含 头文件,并使用 std::thread 类创建和管理线程。

  1. 创建和启动线程
   #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;
   }

  1. 传递参数给线程
   #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;
   }

  1. 多线程计算
   #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;
   }

  1. 互斥锁
    使用互斥锁(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++多线程编程的不同方面,包括创建和启动线程、传递参数给线程、多线程计算以及使用互斥锁来保护共享数据。多线程编程在并发和并行应用程序中非常有用,可以充分利用多核处理器的性能。

在C++中,信号处理是一种用于捕获和处理操作系统发送的信号的机制。信号通常用于通知程序发生了某些事件,例如用户按下Ctrl+C键中断程序执行。以下是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 函数用于向其他进程发送信号。信号处理是处理操作系统级事件的重要方式之一,但在多线程应用程序中需要格外小心,以避免潜在的并发问题。

你可能感兴趣的:(计算机视觉,人工智能)