Linux嵌入式学习——c++学习 ——(3)

一、数组

概念与c语言中得数组相同。                                                                                                                                                                                                                                                                                                                               

二、对象数组

       在C++中,std::vector是一个非常常用的序列容器,它提供了一个动态数组的功能。与普通的数组相比,std::vector具有更高的灵活性,因为它能够根据需要自动调整其大小。这使得std::vector成为处理可变大小数据集合的首选容器之一。

主要特点

  1. 动态大小std::vector能够根据需要自动调整其存储空间的大小,从而可以存储任意数量的元素(受限于可用内存)。

  2. 随机访问:与数组一样,std::vector提供了对元素的随机访问功能,即可以通过下标操作符[]at()成员函数来访问任意位置的元素。

  3. 内存连续std::vector在内部使用一个连续的数组来存储元素,这意呀着它可以高效地进行元素的随机访问,但在某些情况下(如插入或删除元素时)可能会导致内存的重新分配和元素的移动。

  4. 容量与大小std::vector有两个重要的属性:size()capacity()size()返回容器中当前元素的数量,而capacity()返回容器当前分配的存储空间能够容纳的元素数量。容器的capacity()至少与size()一样大,且可能更大,以允许在不重新分配内存的情况下添加更多元素。

就会打印0到999的连续数。

逆序:

#include <vector>
void printfArray(vector<int> &a)
{
    for(auto &x:a)
    {
        cout << x<<endl;
    }

}

void reverseArray(vector<int> &a)
{
    size_t len=a.size();
    for(size_t i=0;i<len/2;++i)
    {
        int t =a[i];
        a[i]=a[len-i-1];
        a[len-i-1]=t;
    }

}

int main(void)
{
    vector<int> vi ={1,2,3,4,5,6,7,8,9,0};

   reverseArray(vi);
    printfArray(vi);

return 0;
}

结合<algorithm>库中的reverse函数,我们可以很方便地实现数组的逆序操作。
reverse函数接受两个迭代器作为参数,分别指向要逆序的序列的开始和结束位置,
注意,结束迭代器是指向序列最后一个元素之后的位置)。

std::begin(arr)std::end(arr)分别生成了指向数组arr第一个元素和最后一个元素之后位置的迭代器。然后,std::reverse函数接受这两个迭代器作为参数,将数组arr中的元素逆序。

#include <iostream>  
#include <vector>  
#include <algorithm>  
  
int main() {  
    std::vector<int> vec = {1, 2, 3, 4, 5};  
  
    // 使用begin和end成员函数逆序vector  
    std::reverse(vec.begin(), vec.end());  
  
    // 打印逆序后的vector  
    for(int elem : vec) {  
        std::cout << elem << " ";  
    }  
    std::cout << std::endl;  
  
    return 0;  
}

迭代器得好处:

可以自己选择想要操作得位置

C++中,使用std::begin(arr)std::end(arr)为数组arr生成迭代器,并将这些迭代器传递给std::reverse函数来逆序数组元素,带来了几个显著的好处:

  1. 类型安全和抽象:迭代器提供了一种类型安全的方式来访问和操作容器(包括数组)中的元素。对于数组,std::begin(arr)std::end(arr)生成的迭代器封装了数组的起始地址和结束地址(实际上是结束地址之后的位置,即“尾后迭代器”)。这种抽象使得代码更加清晰,并且减少了直接处理指针时可能出现的错误。

  2. 可移植性和兼容性:使用迭代器可以使你的代码更加可移植和兼容不同的容器类型。虽然在这个特定的例子中我们是在处理一个原生数组,但std::beginstd::end也适用于std::vectorstd::list等标准库容器。这意味着,如果你将来决定改用不同的容器类型来存储你的数据,你的代码可能只需要很小的修改(甚至不需要修改),因为你可以继续使用相同的迭代器接口来访问和操作数据。

  3. 与标准库算法集成:C++标准库提供了大量的算法,这些算法都设计为接受迭代器作为参数。通过使用std::begin(arr)std::end(arr)生成的迭代器,你可以轻松地将这些算法应用于你的数组上,而无需编写额外的循环或逻辑来处理数组元素。std::reverse就是这样一个例子,它接受一对迭代器作为参数,并逆序该范围内的元素。

  4. 减少错误:直接操作数组的索引或指针很容易出错,特别是当数组边界条件变得复杂时。使用迭代器可以减少这类错误,因为迭代器通常会提供类型检查和边界检查(尽管对于原生数组的迭代器来说,边界检查是可选的,并且通常由程序员负责)。此外,迭代器还允许你使用范围基的for循环(C++11及以后),这可以进一步减少错误并提高代码的可读性。

  5. 清晰的代码结构:使用迭代器可以使你的代码结构更加清晰。通过明确地将算法与数据分离,你可以更容易地理解和维护你的代码。在这个例子中,std::reverse算法负责逆序操作,而std::begin(arr)std::end(arr)则负责提供要操作的数据范围。这种分离使得代码的各个部分更加模块化,并且更容易进行单元测试和维护。

三、指针

c++与C语言中的指针类似,所以介绍一下二者不一样的地方

(1)空指针

1、nullptr

nullptr是C++11标准中引入的一个字面量,用于表示空指针。它提供了一种类型安全的方式来表示空指针,因为nullptr的类型是std::nullptr_t,它可以隐式转换为任何指针类型或指针到成员的类型,但不能隐式转换为整数类型(如intsize_t),这有助于避免一些常见的编程错误 

2、NULL

在C++11之前,NULL通常用于表示空指针。然而,NULL实际上是一个宏定义,通常被定义为((void*)0)或简单的0,这意呀着它可能隐式转换为整数类型,这可能导致一些不期望的行为。因此,在C++11及以后的版本中,推荐使用nullptr而不是NULL

2、函数指针

指针函数是一种特殊的指针,它指向一个函数。这种指针可以存放函数的地址,并允许通过只指针来调用该函数。这种指针可以存储函数的地址,并允许通过指针调用该函数。

(1)、声明函数

返回类型 (*指针名)(参数类型列表);

     int         (*pAdd)(int, int);

 例如:

(2)函数指针的解耦合

             运用函数指针是实现代码解耦合(Decoupling)的一种有效手段。解耦合指的是减少代码模块之间的直接依赖关系,使得它们可以独立地修改和扩展,而不会影响到其他模块,避免修改一个地方,别的很多地方也得改动的情况。

例如:

老板要求得到数组中可以被3整除的数  可以

但如果要是想要得到别的,比如说 被2整除之类的

可以再写一个或者直接改成

极大的增加了代码的复杂性和耦合性,为了减低耦合性,只保留一个,将会增加耦合性的函数写成几个函数,并用指针指向该函数,调用即可。

例子2:

               快速排序qsort

  • void *base 指向要排序的数组的第一个元素的指针。
  • size_t nmemb 是数组中元素的数量。
  • size_t size 是数组中每个元素的大小(以字节为单位)。
  • int (*compar)(const void *, const void *) 是一个指向比较函数的指针,该函数必须接受两个 const void * 类型的参数,并返回一个整数来指示第一个参数是小于、等于还是大于第二个参数。
  • 比较函数必须具有以下特性

  • 如果第一个参数应该出现在第二个参数之前,则返回一个小于零的值。
  • 如果两个参数相等,则返回零。
  • 如果第一个参数应该出现在第二个参数之后,则返回一个大于零的值。

将上面这个改成c++中的函数指针方式。

2、lamda表达式

在C++中,Lambda表达式是一种非常强大且灵活的特性,它允许你定义匿名函数对象,这些对象可以捕获其所在作用域中的变量,并且可以作为参数传递给其他函数或作为返回值。Lambda表达式的基本语法如下:

[capture](parameters) mutable -> return_type { // 函数体 }

  • [capture]捕获列表,指定Lambda表达式体外哪些变量可以在Lambda表达式体内被访问。捕获方式可以是值捕获(通过=或变量名直接指定)或引用捕获(通过&&变量名指定)。如果不写捕获列表,则默认不能捕获任何外部变量。 没有捕获默认为函数指针。
  • (parameters):参数列表,与普通函数的参数列表类似,用于指定Lambda表达式的输入参数。如果不需要参数,可以省略参数列表的圆括号。
  • mutable一个可选的关键字,表示Lambda表达式体内的变量可以被修改(默认情况下,Lambda表达式体内的变量是只读的,即使它们是捕获的变量)。
  • -> return_type:返回类型说明符,用于指定Lambda表达式的返回类型。如果Lambda表达式体只有一个返回语句,并且返回类型可以自动推导,则可以省略返回类型说明符(即使用auto返回类型)。
  • { // 函数体 }:Lambda表达式的函数体,包含Lambda表达式要执行的代码。

将上文的qsort例子转换成lamada表达式

在没有捕获的情况下,lamda可以当作函数的指着来使用。

【capture】的用法

多个想要捕获的数据,可以直接写一个&

         在lambda表达式内部访问类的成员变量或方法时,lambda表达式确实会隐式地捕获其所在类的this引用(如果它是非静态的)。这种捕获是自动发生的,无需显式声明。

用lamada表达式解决刚才说的qsort问题:

四、动态内存的分配

        在C++程序中建立和删除堆对象使用两个运算符:new和delete。

运算符new的功能是动态分配内存,或者称为动态创建堆对象,语法形式为:

        new      数据类型(初始化参数列表);

           

该语句在程序运行过程中申请分配用于存放指定类型数据的内存空间,并根据初始化参数列表中给出的值进行初始化。如果内存申请成功,new运算便返回一个指向新分配内存首地址的类型的指针,可以通过这个指针对堆对象进行访问;如果申请失败,会抛出异常。

        运算符delete用来删除由new建立的对象,释放指针所指向的内存空间。

        delete    指针名;

        如果被删除的是对象,该对象的析构函数将被调用。

       对于用new建立的对象,只能使用delete进行一次删除操作
        如果对同一内存空间多次使用delete进行删除将会导致运行错误。

new运算符动态创建一维数组的语法形式为:

new类型名[数组长度];

其中数组长度指出了数组元素个数,它可以是任何能够得到正整数值的表达式。

malloc及free   与    new和delete的区别

  • new:不仅分配内存,还会调用对象的构造函数(如果有的话)进行初始化。对于数组类型,new会调用对应类型的默认构造函数来初始化数组中的每个元素(如果是类类型的数组)。new操作符会返回一个指向分配的内存的指针,该指针的类型与所分配的对象类型一致。
  • delete:释放new操作符分配的内存,并调用对象的析构函数(如果有的话)进行清理工作。对于数组类型,应使用delete[]来释放内存,并确保析构函数被正确调用。
  • malloc:只负责分配指定大小的内存块,不进行任何初始化。它返回一个void*类型的指针,指向分配的内存。使用malloc时,通常需要手动进行类型转换,将其转换为所需的指针类型。
  • free:释放malloc分配的内存块。
  •             它不会调用任何析构函数或进行其他类型的清理工作。

注意:

 

例子:

例子:

扩容数列:

根据需要动态的扩容数列:

代码:

五、字符串类

(1)初始化字符串

(2)拼接字符串

 

所以选择使用添加一个复制构造函数。

(3)字符串赋值

       在 assign(const String &other) 方法中,通过比较 this 指针和 other 的地址来检查是否发生了自赋值。如果是自赋值,则不执行任何操作,因为对象已经在引用自己,无需复制。

void assign(const char *p) {  
    char *q = new char[strlen(p) + 1]; // 分配新内存  
    strcpy(q, p); // 复制字符串  
    delete[] m_p; // 释放旧内存  
    m_p = q; // 更新指针  

}void assign(const String &other) {  
    if (this != &other) { // 自赋值检查  
        char *q = new char[strlen(other.m_p) + 1]; // 分配新内存  
        strcpy(q, other.m_p); // 复制字符串  
        delete[] m_p; // 释放旧内存  
        m_p = q; // 更新指针  
    }  
    // 如果 this == &other,则不执行任何操作,因为对象已经在引用自己  
}

但是我的代码会导致内存泄漏,为了防止内存泄漏,
在对象销毁的同时,释放相应的空间。

  1. 当 String 对象的生命周期结束时(例如,当对象离开作用域、被显式删除或程序结束时),其析构函数会自动被调用。

  2. 析构函数中的 delete []m_p; 语句负责释放 m_p 指针所指向的内存。注意这里使用了 delete[] 而不是 delete,因为 m_p 是一个指向字符数组的指针,而不是单个字符的指针。使用 delete[] 是正确的,因为它会释放整个数组,并调用数组中每个对象的析构函数(尽管在这个例子中,数组中的元素是 char 类型,它们没有析构函数)。

  3. 释放内存后,m_p 指针变成了悬空指针(dangling pointer),因为它不再指向有效的内存位置。然而,在析构函数中,这通常不是问题,因为对象本身即将被销毁,其所有成员也将不再可用。

  4. 重要的是要注意,一旦析构函数执行完毕,对象就不再存在,因此不应该在析构函数之后访问对象的任何成员(包括 m_p)。

~String() 析构函数是为了确保在 String 对象被销毁时,其内部动态分配的字符串内存(由 m_p 指针指向)也被适当地释放。

深复制和浅复制

浅复制(Shallow Copy)

浅复制是指在对象复制时,仅复制对象中的值,对于对象中的指针成员,仅复制指针本身的值(即内存地址),而不复制指针所指向的内容。这意呀着两个对象中的指针成员将指向同一块内存区域。因此,如果一个对象通过其指针成员修改了内存区域的内容,另一个对象的对应指针成员也会看到这些修改,因为它们都指向同一块内存。

深复制(Deep Copy)

深复制在对象复制时,不仅复制对象中的值,还会复制对象中的指针成员所指向的内容。这通常涉及到为指针所指向的对象动态分配新的内存,并将原对象的内容复制到新分配的内存中。这样,两个对象中的指针成员将指向不同的内存区域,对任一对象的修改都不会影响到另一个对象。

浅复制

String类的所有代码

class String
{
public:
    String(const char *p):m_p(new char[strlen(p)+1])
    {
        strcpy(m_p,p);
    }

    String(const String &other):m_p(new char[strlen(other.m_p)+1])
    {
        strcpy(this->m_p,other.m_p);
    }
    void show() const
    {
        cout<< m_p<< endl;

    }

    ~String()
    {
        delete []m_p;
    }
    void append(const char *p)
    {
        char *q=new char[strlen(m_p)+strlen(p)+1];
        strcpy(q,m_p);
        strcat(q,p);
        delete []m_p;
        m_p=q;

     }
    void append(const String &other)//this //other
    {
        char *p=new char[strlen(this->m_p)+strlen(other.m_p)];
        strcpy(p,this->m_p);
        strcat(p,other.m_p);
        delete []this->m_p;
        this->m_p=p;

    }
    void assin(const char *p)
    {
        char *q=new char[strlen(p)+1];
        strcpy(q,p);
        delete []m_p;
        m_p=q;
    }
    void assin(const String &other)
    {
        if(this !=&other)
        {
        char *q=new char[strlen(other.m_p)+1];
        strcpy(q,other.m_p);
        delete []this->m_p;
       m_p=q;
        }
    }
    size_t lenth()const
    {
        return strlen(m_p);
    }
private:
    char *m_p;
};

六、链表类

(1)创建节点类

这个struct Node定义了一个简单的链表节点。链表是一种常见的数据结构,用于存储一系列的元素,这些元素在内存中不必连续存储。每个元素(在这个上下文中称为“节点”)都包含两部分:一部分用于存储数据(在这个例子中是int类型的data成员),另一部分用于存储指向链表中下一个节点的指针(Node *next成员)。

struct Node
{
    Node(int value =0,Node *p=nullptr):data(value),next(p){}
    int data;
    Node *next;
}

具体来说,Node结构体的定义如下:

  • 构造函数Node(int value =0,Node *p=nullptr) 是一个构造函数,它允许在创建Node对象时初始化其data成员和next指针。如果调用构造函数时没有提供参数,data将默认为0,next将默认为nullptr(即不指向任何节点)。

  • data:这是节点存储的数据,类型为int。你可以根据需要将其更改为其他类型。

  • next:这是一个指向Node类型的指针,用于指向链表中的下一个节点。如果当前节点是链表中的最后一个节点,则next应该为nullptr

(2)构造函数、析构函数、clear成员函数

class List
{
public:
    List():pHead(nullptr){}
    List(const List &other):pHead(nullptr)
    {
        Node *p= other.pHead;
        while(p!=nullptr)
        {
            this->push_back(p->data);
            p=p->next;
        }
    }
    ~List()
    {
        clear();
    }
    void clear(void)
        {
            while (!isEmpty())
            {
                pop_front();
            }
        }

构造函数

  1. 默认构造函数 List():pHead(nullptr){}
    • 这个构造函数没有参数,它将链表的头指针pHead初始化为nullptr,表示这是一个空链表。
  2. 拷贝构造函数 List(const List &other):pHead(nullptr)
    • 这个构造函数接受一个List类型的常量引用other作为参数,用于创建当前对象的一个副本。
    • 它首先将pHead初始化为nullptr,这是为了准备复制过程。
    • 然后,它遍历other链表中的每个节点(通过Node *p= other.pHead;开始,并在循环中p=p->next;递增),使用push_back(p->data);将每个节点的数据复制到新链表的末尾。
    • 这种方式确保了新链表包含与原始链表相同的数据序列。

析构函数

  • ~List() { clear(); }
    • 析构函数负责在List对象被销毁时释放其占用的资源。
    • 在这个例子中,它调用了clear函数来清空链表,从而删除链表中的所有节点并释放它们所占用的内存。

clear函数

  • void clear(void)
    • 这个函数用于删除链表中的所有节点,从而清空链表。
    • 它通过一个循环while (!isEmpty())检查链表是否为空,如果不为空,则调用pop_front();删除链表的第一个节点,直到链表为空为止。
    • 注意,这里假设isEmpty()是一个返回bool值的成员函数,用于检查链表是否为空。

(3)头插

void pop_front(void) {  
    // 首先检查链表是否为空  
    if(!is_Empty()) {  
        // 如果链表不为空,则继续执行  
        Node *p = pHead; // 创建一个指针p,并将其指向链表的头节点(pHead)  
        pHead = p->next; // 将头指针pHead更新为指向下一个节点,即原头节点的下一个节点  
        delete p; // 释放原头节点所占用的内存  
    }  
}
  1. 检查链表是否为空:通过调用is_Empty()函数(这个函数在代码段中没有给出,但我们可以假设它返回一个布尔值,指示链表是否为空)。如果链表为空(即is_Empty()返回true),则不执行任何操作,因为没有什么可以弹出的。

  2. 移除头节点:如果链表不为空,代码首先通过Node *p = pHead;创建一个名为p的指针,并将其初始化为指向链表的头节点(即pHead指向的节点)。然后,通过pHead = p->next;pHead更新为指向原头节点的下一个节点。这样,原头节点就从链表中逻辑上被移除了,尽管它的内存仍然被占用。

  3. 释放内存:最后,通过delete p;释放原头节点所占用的内存。这是非常重要的步骤,因为它防止了内存泄漏。如果不释放内存,那么随着时间的推移,程序可能会消耗越来越多的内存,最终导致性能问题或程序崩溃。

(4)判断为空

检查链表是否为空。如果头节点为nullptr,则返回true,否则返回false

 bool is_Empty() const
    {
        return pHead == nullptr;
    }

(5)求size

返回链表中元素的数量。它通过遍历链表并计数来实现。

 size_t size() const
    {
        size_t cout=0;
        Node *p=pHead;
        while(p)//p!=nullptr
        {
            cout ++;
            p=p->next;
    }
        return cout;
    }

(6)尾插

      在链表的末端插入一个新元素。如果链表为空,则调用push_front。否则,遍历链表直到最后一个节点,然后将新节点的next指针设置为nullptr,并将最后一个节点的next指针指向新节点。

void push_back(int n)
    {
        if(is_Empty())
        {
            push_front(n);
        }
        else{
            Node *pNew = new Node(n);
            Node *p=pHead;
            while(p->next)
            {
                p=p->next;
            }
            p->next=pNew;
        }
    }
  1. 创建新节点:通过new Node(n)创建一个新的节点pNew,该节点包含要插入的值n,并且其next指针初始化为nullptr(虽然在C++中,对于Node类中没有显式初始化next成员的情况,其默认值也会是nullptr,但明确初始化是一个好习惯)。

  2. 遍历链表找到最后一个节点:声明一个Node类型的指针p,并将其初始化为指向链表的头节点pHead。然后,使用一个while循环遍历链表,直到p->nextnullptr,即找到了链表的最后一个节点。在这个循环中,p被不断更新为指向当前节点的下一个节点,直到它指向链表的最后一个节点。

  3. 将新节点插入到链表末尾:在找到链表的最后一个节点后,将它的next指针指向新创建的节点pNew,从而将新节点插入到链表的末尾。

(7)头删

      移除链表的前端元素,并释放其占用的内存。如果链表不为空,它将更新头节点为第二个节点,并删除原来的头节点

 void pop_front(void)
    {
        if(!is_Empty())
        {
            Node *p=pHead;
            pHead=p->next;
            delete p;
        }
    }

(8)尾删

        移除链表的末端元素,并释放其占用的内存。如果链表至少有两个元素,它遍历到倒数第二个节点,然后删除最后一个节点,并将倒数第二个节点的next指针设置为nullptr。如果链表只有一个元素,则调用pop_front

void pop_back(void)
    {
        if(size()>=2)
        {
            Node *p=pHead;
            while(p->next->next){
                p=p->next;
            }
            delete p->next;
            p->next=nullptr;
        }
        else
        {
            pop_front();
        }
    }

这样创造出来的链表还是会有内存泄漏,所以在析构函数中做到

只要链表不为空,就删除。

如果为空的话,采用头删,因为头删的算法复杂度为1,尾删的算法复杂度为n。

具体步骤:

但是会带来另外一个问题

:深复制和浅复制的问题

(9)list类代码

struct Node
{
    Node(int value =0,Node *p=nullptr):data(value),next(p){}
    int data;
    Node *next;
};
class List
{
public:
    List():pHead(nullptr){}
    List(const List &other):pHead(nullptr)
    {
        Node *p= other.pHead;
        while(p!=nullptr)
        {
            this->push_back(p->data);
            p=p->next;
        }
    }
    ~List()
    {
        clear();
    }
    void clear(void)
        {
            while (!isEmpty())
            {
                pop_front();
            }
        }
    void push_front(int n)
    {
        Node *pNew =new Node(n);
        pNew->next=pHead;
        pHead= pNew;
    }
    size_t size() const
    {
        size_t cout=0;
        Node *p=pHead;
        while(p)//p!=nullptr
        {
            cout ++;
            p=p->next;
    }
        return cout;
    }
    void push_back(int n)
    {
        if(is_Empty())
        {
            push_front(n);
        }
        else{
            Node *pNew = new Node(n);
            Node *p=pHead;
            while(p->next)
            {
                p=p->next;
            }
            p->next=pNew;
        }
    }
    void pop_front(void)
    {
        if(!is_Empty())
        {
            Node *p=pHead;
            pHead=p->next;
            delete p;
        }
    }
    void pop_back(void)
    {
        if(size()>=2)
        {
            Node *p=pHead;
            while(p->next->next){
                p=p->next;
            }
            delete p->next;
            p->next=nullptr;
        }
        else
        {
            pop_front();
        }
    }
    bool is_Empty() const
    {
        return pHead == nullptr;
    }
    void show()
    {
        Node *p=pHead;
        while(p)
        {
            cout<<p->data<<",";
            p=p->next;
        }
        cout <<"\b\n";
    }

private:
    Node *pHead;

};



int main(void)
{
    List l;
    cout<<"size="<<l.size()<<endl;
   cout<< "empty="<<l.is_Empty()<<endl;
    l.push_front(1);
    l.push_front(2);
    l.push_front(3);
 //l.show();
    l.push_back(4);
    l.push_back(5);
    l.push_back(6);
    l.show();
    //l.pop_front();
    l.pop_back();
    l.show();
    cout<<"size="<<l.size()<<endl;
   cout<< "empty="<<l.is_Empty()<<endl;

  • 24
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值