一、数组
概念与c语言中得数组相同。
二、对象数组
在C++中,std::vector
是一个非常常用的序列容器,它提供了一个动态数组的功能。与普通的数组相比,std::vector
具有更高的灵活性,因为它能够根据需要自动调整其大小。这使得std::vector
成为处理可变大小数据集合的首选容器之一。
主要特点
-
动态大小:
std::vector
能够根据需要自动调整其存储空间的大小,从而可以存储任意数量的元素(受限于可用内存)。 -
随机访问:与数组一样,
std::vector
提供了对元素的随机访问功能,即可以通过下标操作符[]
或at()
成员函数来访问任意位置的元素。 -
内存连续:
std::vector
在内部使用一个连续的数组来存储元素,这意呀着它可以高效地进行元素的随机访问,但在某些情况下(如插入或删除元素时)可能会导致内存的重新分配和元素的移动。 -
容量与大小:
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
函数来逆序数组元素,带来了几个显著的好处:
类型安全和抽象:迭代器提供了一种类型安全的方式来访问和操作容器(包括数组)中的元素。对于数组,
std::begin(arr)
和std::end(arr)
生成的迭代器封装了数组的起始地址和结束地址(实际上是结束地址之后的位置,即“尾后迭代器”)。这种抽象使得代码更加清晰,并且减少了直接处理指针时可能出现的错误。可移植性和兼容性:使用迭代器可以使你的代码更加可移植和兼容不同的容器类型。虽然在这个特定的例子中我们是在处理一个原生数组,但
std::begin
和std::end
也适用于std::vector
、std::list
等标准库容器。这意味着,如果你将来决定改用不同的容器类型来存储你的数据,你的代码可能只需要很小的修改(甚至不需要修改),因为你可以继续使用相同的迭代器接口来访问和操作数据。与标准库算法集成:C++标准库提供了大量的算法,这些算法都设计为接受迭代器作为参数。通过使用
std::begin(arr)
和std::end(arr)
生成的迭代器,你可以轻松地将这些算法应用于你的数组上,而无需编写额外的循环或逻辑来处理数组元素。std::reverse
就是这样一个例子,它接受一对迭代器作为参数,并逆序该范围内的元素。减少错误:直接操作数组的索引或指针很容易出错,特别是当数组边界条件变得复杂时。使用迭代器可以减少这类错误,因为迭代器通常会提供类型检查和边界检查(尽管对于原生数组的迭代器来说,边界检查是可选的,并且通常由程序员负责)。此外,迭代器还允许你使用范围基的for循环(C++11及以后),这可以进一步减少错误并提高代码的可读性。
清晰的代码结构:使用迭代器可以使你的代码结构更加清晰。通过明确地将算法与数据分离,你可以更容易地理解和维护你的代码。在这个例子中,
std::reverse
算法负责逆序操作,而std::begin(arr)
和std::end(arr)
则负责提供要操作的数据范围。这种分离使得代码的各个部分更加模块化,并且更容易进行单元测试和维护。
三、指针
c++与C语言中的指针类似,所以介绍一下二者不一样的地方
(1)空指针
1、nullptr
nullptr
是C++11标准中引入的一个字面量,用于表示空指针。它提供了一种类型安全的方式来表示空指针,因为nullptr
的类型是std::nullptr_t
,它可以隐式转换为任何指针类型或指针到成员的类型,但不能隐式转换为整数类型(如int
或size_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,则不执行任何操作,因为对象已经在引用自己
}
但是我的代码会导致内存泄漏,为了防止内存泄漏,
在对象销毁的同时,释放相应的空间。
当
String
对象的生命周期结束时(例如,当对象离开作用域、被显式删除或程序结束时),其析构函数会自动被调用。析构函数中的
delete []m_p;
语句负责释放m_p
指针所指向的内存。注意这里使用了delete[]
而不是delete
,因为m_p
是一个指向字符数组的指针,而不是单个字符的指针。使用delete[]
是正确的,因为它会释放整个数组,并调用数组中每个对象的析构函数(尽管在这个例子中,数组中的元素是char
类型,它们没有析构函数)。释放内存后,
m_p
指针变成了悬空指针(dangling pointer),因为它不再指向有效的内存位置。然而,在析构函数中,这通常不是问题,因为对象本身即将被销毁,其所有成员也将不再可用。重要的是要注意,一旦析构函数执行完毕,对象就不再存在,因此不应该在析构函数之后访问对象的任何成员(包括
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();
}
}
构造函数
- 默认构造函数
List():pHead(nullptr){}
- 这个构造函数没有参数,它将链表的头指针
pHead
初始化为nullptr
,表示这是一个空链表。
- 这个构造函数没有参数,它将链表的头指针
- 拷贝构造函数
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; // 释放原头节点所占用的内存
}
}
检查链表是否为空:通过调用
is_Empty()
函数(这个函数在代码段中没有给出,但我们可以假设它返回一个布尔值,指示链表是否为空)。如果链表为空(即is_Empty()
返回true
),则不执行任何操作,因为没有什么可以弹出的。移除头节点:如果链表不为空,代码首先通过
Node *p = pHead;
创建一个名为p
的指针,并将其初始化为指向链表的头节点(即pHead
指向的节点)。然后,通过pHead = p->next;
将pHead
更新为指向原头节点的下一个节点。这样,原头节点就从链表中逻辑上被移除了,尽管它的内存仍然被占用。释放内存:最后,通过
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;
}
}
创建新节点:通过
new Node(n)
创建一个新的节点pNew
,该节点包含要插入的值n
,并且其next
指针初始化为nullptr
(虽然在C++中,对于Node
类中没有显式初始化next
成员的情况,其默认值也会是nullptr
,但明确初始化是一个好习惯)。遍历链表找到最后一个节点:声明一个
Node
类型的指针p
,并将其初始化为指向链表的头节点pHead
。然后,使用一个while
循环遍历链表,直到p->next
为nullptr
,即找到了链表的最后一个节点。在这个循环中,p
被不断更新为指向当前节点的下一个节点,直到它指向链表的最后一个节点。将新节点插入到链表末尾:在找到链表的最后一个节点后,将它的
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;