数组
数组的定义与使用
数组是具有一定顺序关系的若干相同类型变量的集合体,组成数组的变量称为该数组的元素。
给出下面程序的输出:
#include <iostream>
using namespace std;
int main() {int a[10], b[10];for(int i = 0; i < 10; i++) {a[i] = i * 2 - 1;b[10 - i - 1] = a[i];}for (const auto &e:a) //范围for循环,输出a中每个元素cout << e << " ";cout << endl; for (int i = 0; i <10; i++) //下标迭代循环,输出b中每个元素cout << b[i] << " ";cout << endl;return 0;
}
数组的存储与初始化
数组作为函数参数
看例题6-2及其之后的
对象数组
举例:
//Point.h
#ifndef _POINT_H
#define _POINT_H
class Point { //类的定义
public: //外部接口Point();Point(int x, int y);~Point();void move(int n ewX,int newY);int getX() const { return x; }int getY() const { return y; }static void showCount(); //静态函数成员
private: //私有数据成员int x, y;
};
#endif //_POINT_H
//Point.cpp
#include <iostream>
#include "Point.h"
using namespace std;
Point::Point() : x(0), y(0) {cout << "Default Constructor called." << endl;
}
Point::Point(int x, int y) : x(x), y(y) {cout << "Constructor called." << endl;
}
Point::~Point() {cout << "Destructor called." << endl;
}
void Point::move(int newX,int newY) {cout << "Moving the point to (" << newX << ", " << newY << ")" << endl;x = newX;y = newY;
}
//6-3.cpp
#include "Point.h"
#include <iostream>
using namespace std;int main() {cout << "Entering main..." << endl;Point a[2];for(int i = 0; i < 2; i++)a[i].move(i + 10, i + 20);cout << "Exiting main..." << endl;return 0;
}
运行结果:
Entering main...
Default Constructor called.
Default Constructor called.
Moving the point to (10, 20)
Moving the point to (11, 21)
Exiting main...
Destructor called.
Destructor called.
指针
指针的概念和定义、与地址相关的运算
内存空间的访问方式:
- 通过变量名访问
- 通过地址访问
地址运算符:&
例:int var;
&var
表示变量 var 在内存中的起始地址
指针的概念
- 指针:内存地址,用于间接访问内存单元
- 指针变量:用于存放地址的变量
指针的初始化和赋值
注意:“不要用一个内部非静态变量去初始化 static 指针。”
这句话的意思是,不要将一个在函数内部定义的非静态变量的地址赋值给静态指针。这样做可能会导致指针指向的内存区域在函数执行完毕后被释放,但静态指针仍然持有该地址,这会导致静态指针指向的内存区域已经不再有效。
具体来说,如果将一个函数内部的非静态局部变量的地址赋值给静态指针,并在函数结束后继续使用这个静态指针,那么静态指针将指向一个已经被销毁的内存区域,这会导致未定义的行为。或者这个“已经被销毁的内存区域”存有新的数据,但是可以通过指针操作这个数据,是很危险的行为。
指针变量的赋值运算
- 语法形式
指针名=地址
注意:“地址”中存放的数据类型与指针类型必须相符
- 向指针变量赋的值必须是地址常量或变量,不能是普通整数。
例如:
通过地址运算“&”求得已定义的变量和对象的起始地址
动态内存分配成功时返回的地址
例外:整数0可以赋给指针,表示空指针。
允许定义或声明指向 void 类型的指针。该指针可以被赋予任何类型对象的地址。
例: void *general;
指针空值nullptr
- 以往用0或者NULL去表达空指针的问题:
C/C++的NULL宏是个有很多潜在BUG的宏。因为有的库把其定义成整数0,有的定义成 (void*)0。在C的时代还
好。但是在C++的时代,这就会引发很多问题。
- C++11使用nullptr关键字,是表达更准确,类型安全的空指针
指向常量的指针
不能通过指向常量的指针改变所指对象的值,但指针本身可以改变,可以指向另外的对象。
例
int a;const int *p1 = &a; //p1是指向常量的指针int b;p1 = &b; //正确,p1本身的值可以改变*p1 = 1; //编译时出错,不能通过p1改变所指的对象
指针类型的常量
若声明指针常量,则指针本身的值不能被改变。
例
int a;int * const p2 = &a; int b;p2 = &b; //错误,p2是指针常量,值不能改变
指针的运算
指针类型的算术运算
指针与整数的加减运算
指针++,–运算
- 指针p加上或减去n,其意义是指针当前指向位置的前方或后方第n个数据的起始位置。
- 指针的++、–运算,意义是指向下一个或前一个完整数据的起始。
- 运算的结果值取决于指针指向的数据类型,总是指向一个完整数据的起始位置。
- 当指针指向连续存储的同类型数据时,指针与整数的加减运和自增自减算才有意义。
指针类型的关系运算
-
指向相同类型数据的指针之间可以进行各种关系运算。
-
指向不同数据类型的指针,以及指针与一般整数变量之间的关系运算是无意义的。
-
指针可以和零之间进行等于或不等于的关系运算。
例如:p==0或p!=0
补充:
指针类型的关系运算在实际编程中有多种应用场景,其中一些包括:
- 数据结构的遍历和操作: 在链表、树等数据结构中,使用指针进行节点之间的连接和遍历。通过比较指针的大小关系,可以确定节点的相对位置,从而进行插入、删除、搜索等操作。例如,在二叉搜索树中,比较指针的大小关系可以确定节点的插入位置,保持树的有序性。
- 内存管理: 在动态内存分配和释放中,指针类型的关系运算可以帮助程序员确保内存的正确使用和释放。比较指针是否为NULL可以检查指针是否已经指向了有效的内存区域,以避免访问未分配或已释放的内存。
- 多线程编程: 在多线程编程中,使用指针来共享数据时,需要确保对共享数据的访问是线程安全的。指针类型的关系运算可以帮助程序员控制对共享资源的访问顺序和频率,从而避免竞态条件和数据竞争。
- 数组和字符串操作: 指针类型的关系运算在数组和字符串操作中也很常见。比如,在字符串比较函数中,可以通过比较两个指针指向的字符来确定字符串的大小关系,从而进行字符串的排序、查找等操作。
- 性能优化: 在一些需要高效处理大量数据的场景中,比如图形处理、网络通信等,指针类型的关系运算可以帮助程序员优化算法和数据结构的设计,从而提高程序的性能和效率。
用指针处理数组元素
数组是一组连续存储的同类型数据,可以通过指针的算术运算,使指针依次指向数组的各个元素,进而可以遍历数组。
例6-7
设有一个int型数组a,有10个元素。用三种方法输出各元素:
- 使用数组名和下标
- 使用数组名和指针运算
- 使用指针变量
#include <iostream>
using namespace std;
int main(){int a[10] = {1,2,3,4,5,6,7,8,9,10};//使用数组名和下标for(int i = 0;i < 10;i++){cout << a[i] << " ";}cout << endl;//使用数组名和指针运算for(int j = 0;j < 10;j++){cout << *(a+j) << " ";}cout << endl;//使用指针变量for(int *pa = a;pa < (a+10);pa++){cout << *pa << " ";}cout << endl;return 0;
}
以指针作为函数参数
为什么需要用指针做参数?
- 需要数据双向传递时(引用也可以达到此效果)
- 用指针作为函数的参数,可以使被调函数通过形参指针存取主调函数中实参指针指向的数据,实现数据的双向传递
- 需要传递一组数据,只传首地址运行效率比较高
- 实参是数组名时形参可以是指针
例6-10
例: 指向常量的指针做形参
#include<iostream>
using namespace std;
const int N = 6;
void print(const int *p, int n);
int main() {int array[N];for (int i = 0; i < N; i++)cin>>array[i];print(array, N);return 0;
}
void print(const int *p, int n) {cout << "{ " << *p; //注意为什么要先输出*p,而且下面的循环是从i=1开始的for (int i = 1; i < n; i++)cout << ", " << *(p+i);cout << " }" << endl;
}
指针类型的函数
指针函数的定义形式(区分指针函数和函数指针)
存储类型 数据类型 *函数名()
{ //函数体语句
}
注意:
- 不要将非静态局部地址用作函数的返回值
- 错误的例子:在子函数中定义局部变量后将其地址返回给主函数,就是非法地址
下面是一个错误的示例:
int main(){int* function();int* ptr= function();*prt=5; //危险的访问!return 0;
}
int* function(){int local=0; //非静态局部变量作用域和寿命都仅限于本函数体内return &local;
}//函数运行结束时,变量local被释放
错误的地方:
- 在
main
函数中声明了一个名为function
的函数,但是没有提供函数的定义或者声明。应该在main
函数之前提供function
函数的声明或者定义,否则编译器无法识别。 - 在
function
函数中,将一个指向局部变量local
的指针返回给调用者。但是一旦function
函数执行完毕,局部变量local
将被销毁,其内存空间将被释放,而返回的指针将变为悬空指针,指向的内存空间将不再有效。 - 在
main
函数中,将function
返回的指针赋值给了ptr
,然后尝试通过*ptr=5;
来修改指针所指向的内存空间的值。然而,由于该内存空间已经被释放,这是一种危险的访问操作,会导致未定义的行为,可能导致程序崩溃或产生不可预测的结果。
为了修正这些问题,可以将 function
函数改为动态分配内存,并在使用完毕后手动释放。例如:
#include <iostream>int* function() {int* local = new int(0);return local;
}int main() {int* function();int* ptr = function();*ptr = 5; // 正确的访问,因为内存是动态分配的delete ptr; // 释放动态分配的内存return 0;
}
返回的指针要确保在主调函数中是有效、合法的地址
正确的例子:
主函数中定义的数组,在子函数中对该数组元素进行某种操作后,返回其中一个元素的地址,这就是合法有效的地址.下面是正确的例子:
#include<iostream>
using namespace std;
int main(){int array[10]; //主函数中定义的数组int* search(int* a, int num);for(int i=0; i<10; i++)cin>>array[i];int* zeroptr= search(array, 10); //将主函数中数组的首地址传给子函数return 0;
}
int* search(int* a, int num){ //指针a指向主函数中定义的数组for(int i=0; i<num; i++)if(a[i]==0)return &a[i]; //返回的地址指向的元素是在主函数中定义的return 0;
}//函数运行结束时,a[i]的地址仍有效
返回的指针要确保在主调函数中是有效、合法的地址
正确的例子:
在子函数中通过动态内存分配new操作取得的内存地址返回给主函数是合法有效的,但是内存分配和释放不在同一级别,要注意不能忘记释放,避免内存泄漏
正确的代码:
#include<iostream>
using namespace std;
int main(){int* newintvar();int* intptr= newintvar();*intptr=5; //访问的是合法有效的地址delete intptr; //如果忘记在这里释放,会造成内存泄漏return 0;
}
int* newintvar (){ int* p=new int();return p; //返回的地址指向的是动态分配的空间
}//函数运行结束时,p中的地址仍有效
注意这个正确的代码和前面的错误代码的区别:
在后一个代码中,函数 newintvar
动态分配了内存并返回了指向该内存的指针,所以 *intptr=5;
访问的是合法有效的内存地址,因为该地址是由 new
运算符分配的动态内存,而且程序员必须在适当的时候使用 delete
运算符来释放该内存,否则会导致内存泄漏。
而在前一个代码中,函数 function
返回了一个指向局部变量 local
的指针,这是不正确的。因为 local
是在函数内部定义的局部变量,其生命周期仅限于函数执行期间。一旦函数执行结束,local
将被销毁,其内存空间将被释放。因此,返回的指针将指向一个已经释放的内存地址,称为悬空指针。尝试通过悬空指针访问内存是未定义行为,可能导致程序崩溃或产生不可预测的结果。
函数指针
见另外一个文档记录
对象指针
对象指针定义形式
类名 *对象指针名;
例:
Point a(5,10);Piont *ptr;ptr=&a;
通过指针访问对象成员
对象指针名->成员名
ptr->getx()
相当于(*ptr).getx();
举例:
#include <iostream>
using namespace std;
class Point{
public:Point(int x,int y):x(x),y(y){}int getX(){return x;}int getY(){return y;}
private:int x,y;
};
int main(){Point p1(4,5);Point *p2 = &p1;cout << "p2.getX():" << p2->getX() << endl;cout << "p2.getX():" << (*p2).getX() << endl;cout << "p1.getX():" << p1.getX() << endl;return 0;
}
指针数组
数组的元素是指针型
例:Point *pa[2];由pa[0],pa[1]两个指针组成
例6-8 利用指针数组存放矩阵
#include <iostream>
using namespace std;
int main(){int line1[] = {1,0,0};int line2[] = {0,1,0};int line3[] = {0,0,1};//定义一个指针数组int *ptr[3] = {line1,line2,line3};cout << "Matrix test:" << endl;//输出矩阵for(int i = 0;i < 3;i++){for(int j = 0;j < 3;j++){cout << ptr[i][j] << " " ;}cout << endl;}return 0;
}
指针数组与二维数组对比
根据下图,可以得出结论:二维数组是一个连续的存储块,而指针数组的每个指针可以指向不同内存位置。
C++中的指针数组和二维数组虽然在某些方面相似,但在其他方面有着明显的区别。
-
指针数组(Pointer Array):
- 指针数组是一个数组,其中的每个元素都是指针。
- 每个指针可以指向不同类型或相同类型的数据。
- 指针数组的大小可以在运行时动态改变。
- 通过指针数组可以方便地操作多个不同类型的数据。
int* ptrArray[5]; // 定义一个包含5个指针的数组
-
二维数组(Two-dimensional Array):
- 二维数组是一个数组,其中的每个元素又是一个数组(或称为行)。
- 所有的行都有相同的列数,形成矩阵结构。
- 二维数组的大小在编译时必须确定,并且在运行时无法改变。
- 访问二维数组的元素时,需要使用两个索引值。
int twoDArray[3][4]; // 定义一个3行4列的二维数组
主要区别在于指针数组的每个元素都是指针,而二维数组的每个元素都是数据,且二维数组是一个连续的存储块,而指针数组的每个指针可以指向不同内存位置。在使用时,要根据具体需求选择合适的数据结构。
this指针
隐含于类的每一个非静态成员函数中。
指出成员函数所操作的对象。
当通过一个对象调用成员函数时,系统先将该对象的地址赋给this指针,然后调用成员函数,成员函数对对象的数据成员进行操作时,就隐含使用了this指针。
例如:Point类的getX函数中的语句:
return x;
相当于:
return this->x;
曾经出现的错误的例子:
class Fred; //前向引用声明
class Barney {Fred x; //错误:类Fred的声明尚不完善
};
class Fred {Barney y;
};
错误的原因:
在错误的代码中,类 Barney 中包含一个 Fred 类型的成员变量 x。但是在 Barney 类的声明之后,Fred 类的声明之前,编译器无法识别 Fred 类,因此无法知道 Fred 类的大小。因此,在声明 Barney 类时,Fred 类的声明尚不完善,编译器会报错。
正确的代码:
class Fred; //前向引用声明
class Barney {Fred *x; };
class Fred {Barney y;};
在正确的代码中,Barney
类中包含一个 Fred
类型的指针成员变量 x
,而不是直接声明 Fred
类型的对象。由于指针的大小在编译时是已知的,因此编译器不需要知道 Fred
类的完整定义,只需要知道指针的大小即可。因此,即使在 Barney
类的声明之后,Fred
类的声明之前,编译器也能够顺利通过编译。
前向引用
关于前面举到的2个例子:
尽管使用了前向引用声明,但是在提供了一个完整的类定义之前,不能定义该类的对象,也不能在成员函数中使用该对象,但是作为函数的形参是可以的,例如:
class B;class A
{
public:
void function(B b);
};class B
{
public:
void function2(A a);
};
智能指针
具备特殊功能的指针类。
内置的智能指针:用于解决内存泄露的一种指针自动回收机制(引用计数法):
- unique_ptr:只允许被引用一次,作用域结束后自动回收 C++11
- shared_ptr:可以被共享引用,其内存在一个引用计数器,计数器为0时自动回收。 C ++11
- weak_ptr:类似弱引用,查看是否被回收,如果没有被回收,还能再用一次 C++11
- (C++98)auto_ptr:auto_ptr 是c++ 98定义的智能指针模板(已经弃用,不建议使用),其定义了管理指针的对象,可以将new 获得(直接或间接)的地址赋给这种对象。当对象过期时,其析构函数将使用delete 来释放内存!
auto_ptr
用法:头文件: #include < memory >用 法: auto_ptr<类型> 变量名(new 类型)
auto_ptr已经弃用,可以使用unique_ptr
来代替。
unique_ptr
#include <iostream>
#include <memory>
using namespace std;int main(){unique_ptr<string> str(new string("习"));cout << str << endl;//输出地址cout << *str << endl;//输出内容return 0;
}
执行结果:
0x600000204780
习
Program ended with exit code: 0
shared_ptr
举例:
#include <iostream>
#include <memory> // 包含头文件int main() {// 创建一个 shared_ptr 智能指针,指向一个动态分配的整数对象std::shared_ptr<int> ptr1(new int(5));// 可以通过 get() 方法获取原始指针int* rawPtr = ptr1.get();std::cout << "Value of ptr1: " << *rawPtr << std::endl;// 创建另一个 shared_ptr 智能指针,指向同一个对象std::shared_ptr<int> ptr2 = ptr1;// 打印两个智能指针的引用计数std::cout << "Reference count of ptr1: " << ptr1.use_count() << std::endl;std::cout << "Reference count of ptr2: " << ptr2.use_count() << std::endl;// 使用智能指针进行内存管理,不需要手动释放// ...return 0;
} // 在 main 函数结束时,所有智能指针都会自动释放分配的内存
执行结果:
Value of ptr1: 5
Reference count of ptr1: 2
Reference count of ptr2: 2
Program ended with exit code: 0
注:
use_count()
方法用于获取与shared_ptr
关联的引用计数,即当前指向对象的shared_ptr
的数量。这个方法返回一个整数值,表示有多少个shared_ptr
指向同一个对象。
weak_ptr
weak_ptr
是 C++ 中智能指针的一种,它通常用于解决 shared_ptr
的循环引用问题,同时避免产生悬空指针。weak_ptr
本身不增加引用计数,因此它不会导致对象的生命周期延长。
#include <iostream>
#include <memory>class Node; // 前向声明class Parent {
public:std::shared_ptr<Node> child; // 使用 shared_ptr 指向 Node 对象
};class Node {
public:std::weak_ptr<Parent> parent; // 使用 weak_ptr 指向 Parent 对象
};int main() {// 创建 Parent 和 Node 对象std::shared_ptr<Parent> parentPtr = std::make_shared<Parent>();std::shared_ptr<Node> nodePtr = std::make_shared<Node>();// 在 Parent 对象中保存 Node 对象的 shared_ptrparentPtr->child = nodePtr;// 在 Node 对象中保存 Parent 对象的 weak_ptrnodePtr->parent = parentPtr;// 使用 weak_ptr 访问 Parent 对象if (auto parent = nodePtr->parent.lock()) {std::cout << "Node's parent exists." << std::endl;} else {std::cout << "Node's parent is null." << std::endl;}// 当 parentPtr 被销毁后,nodePtr->parent 会自动失效parentPtr.reset();// 使用 weak_ptr 访问 Parent 对象if (auto parent = nodePtr->parent.lock()) {std::cout << "Node's parent exists." << std::endl;} else {std::cout << "Node's parent is null." << std::endl;}return 0;
}
执行结果:
Node's parent exists.
Node's parent is null.
Program ended with exit code: 0
注意:weak_ptr的lock方法
lock()
方法用于将weak_ptr
转换为shared_ptr
,以访问weak_ptr
所指向的对象。如果weak_ptr
所指向的对象还存在,则lock()
方法返回一个有效的shared_ptr
,可以安全地访问对象。如果weak_ptr
所指向的对象已经被释放,则lock()
方法返回一个空的shared_ptr
。
动态内存分配
参考阅读:https://bbs.huaweicloud.com/blogs/289405
C/C++ 中的动态内存分配是指由程序员手动进行内存分配。对于“int a”、“char str[10]”
等普通变量,内存会自动分配和释放。对于像“int *p = new int[10]
”这样的动态分配内存,程序员有责任在不再需要时释放内存。如果程序员不释放内存,则会导致内存泄漏(直到程序终止内存才会释放)。
内存分为两部分:
- 堆栈
- 堆
在堆栈中,函数内部声明的所有变量都从堆栈中占用内存。
堆是程序未使用的内存,用于在程序运行时动态分配内存。
因此,动态分配的内存在堆上分配,非静态和局部变量在堆栈上分配内存。
C 使用malloc()
和calloc()
函数在运行时动态分配内存,并使用free()
函数释放动态分配的内存。C++ 支持这些函数,并且还有两个运算符new和delete,它们以更好、更简单的方式执行分配和释放内存的任务。
new关键字
new 类型名T(初始化参数)
功能:
在程序执行期间,申请用于存放T类型对象的内存空间,并依初始化参数进行初始化。
基本类型初始化:如果有初始化参数,依初始化参数进行初始化;如果没有括号和初始化参数,不进行初始化,新分配的内存中内容不确定;如果有括号但初始化参数为空,初始化为0。
对象类型:如果有初始化参数,以初始化参数中的值为参数调用构造函数进行初始化;如果没有括号和初始化参数或者有括号但初始化参数为空,用默认构造函数初始化。
结果值:成功:T类型的指针,指向新分配的内存;失败:抛出异常。
new 类型名T [ 表达式 ] [ 常量表达式 ]…… ()
功能:
在程序执行期间,申请用于存放T类型对象数组的内存空间,可以有“()”但初始化列表必须为空。
如果有“()”,对每个元素的初始化与执行“new T()”所做进行初始化的方式相同。
如果没有“()”,对每个元素的初始化与执行“new T”所做进行初始化的方式相同。
结果值:
如果内存申请成功,返回一个指向新分配内存首地址的指针。
例如:
double* array=new double[n]();char (*fp)[3];//声明了一个指针 fp,指向一个包含三个字符的数组fp = new char[n][3];
如果失败:抛出异常。
delete关键字
delete 指针p
功能:释放指针p所指向的内存。p必须是new操作的返回值。
delete[] 指针p
功能:释放指针p所指向的数组。p必须是用new分配得到的数组首地址。
举例:
#include <iostream>
using namespace std;
class Point{
public:
Point():x(0),y(0){cout << "default constructor called !!" << endl;
}
Point(int x,int y):x(x),y(0){cout << "constructor called!!" << endl;
}~Point(){cout << "destructor called !!" << endl;}int getX(){return x;}int getY(){return y;}void move(int newX,int newY){x = newX;y = newY;}
private:int x,y;//表示坐标x,y
};int main(){Point *ptr = new Point;delete ptr;ptr = new Point(1,2);delete ptr;return 0;
}
执行结果:
default constructor called !!
destructor called !!
constructor called!!
destructor called !!
如果是对象数组:
#include <iostream>
using namespace std;
class Point{
public:
Point():x(0),y(0){cout << "default constructor called !!" << endl;
}
Point(int x,int y):x(x),y(0){cout << "constructor called!!" << endl;
}~Point(){cout << "destructor called !!" << endl;}int getX(){return x;}int getY(){return y;}void move(int newX,int newY){x = newX;y = newY;cout << "move to "<< x << "," << y << endl;}
private:int x,y;//表示坐标x,y
};int main(){Point *ptr = new Point[2];ptr[0].move(1,2);ptr[1].move(3,4);cout << "deleting....." << endl;delete[] ptr;
}
执行结果:
default constructor called !!
default constructor called !!
move to 1,2
move to 3,4
deleting.....
destructor called !!
destructor called !!
动态创建多维数组
#include <iostream>int main() {int rows = 3; // 行数int cols = 4; // 列数// 动态分配内存int **arr = new int*[rows]; // 分配行指针数组for (int i = 0; i < rows; ++i) {arr[i] = new int[cols]; // 分配每一行的列数组}// 初始化数组int count = 0;for (int i = 0; i < rows; ++i) {for (int j = 0; j < cols; ++j) {arr[i][j] = count++;}}// 打印数组for (int i = 0; i < rows; ++i) {for (int j = 0; j < cols; ++j) {std::cout << arr[i][j] << " ";}std::cout << std::endl;}// 释放内存for (int i = 0; i < rows; ++i) {delete[] arr[i]; // 释放每一行的列数组}delete[] arr; // 释放行指针数组return 0;
}
执行结果:
0 1 2 3
4 5 6 7
8 9 10 11
将动态数组封装成类
更加简洁,便于管理
建立和删除数组的过程比较繁琐
封装成类后更加简洁,便于管理
可以在访问数组元素前检查下标是否越界
用assert来检查,assert只在调试时生效
#include <iostream>
using namespace std;
class Point{
public:Point();Point(int x,int y);~Point(){cout << "destructor" << endl;}int getX(){return x;}int getY(){return y;}void move(int newX,int newY){x = newX;y = newY;cout << " x move to " << x << ", y move to " << y << endl;}
private:int x,y;
};
Point::Point():x(0),y(0){cout << "default constructor called" << endl;
}
Point::Point(int x,int y):x(x),y(y){cout << "constructor called" << endl;
}
//动态数组封装成类
class ArrayOfPoint{
public:ArrayOfPoint(int size):size(size){points = new Point[size];}//析构函数~ArrayOfPoint(){cout << "deleting" << endl;delete[] points;}int getSize(){return size;}Point& element(int index){//注意返回的是引用类型assert(index>=0&&index<size);return points[index];}private:Point *points;int size;
};
int main(){int count;cout << "input the count of points:" << endl;cin >> count;ArrayOfPoint points(count);points.element(0).move(4,5);points.element(1).move(5,6);return 0;
}
执行结果:
input the count of points:
2
default constructor called
default constructor calledx move to 4, y move to 5x move to 5, y move to 6
deleting
destructor
destructor
Program ended with exit code: 0
思考:为什么element函数返回对象的引用?
返回“引用”可以用来操作封装数组对象内部的数组元素。如果返回“值”则只是返回了一个“副本”,通过“副本”是无法操作原来数组中的元素的
用vector创建数组对象
例如一道leetcode的一道“两数之和”的题,使用哈希表就是下面的解法:
class Solution {
public:vector<int> twoSum(vector<int>& nums, int target) {unordered_map<int,int> hashtable;for(int i = 0;i < nums.size();i++){auto it = hashtable.find(target-nums[i]);if(it!=hashtable.end()){return {i,it->second};}hashtable[nums[i]] = i;}return {};}
};
vector常用的API
1.size()方法,返回个数
2.变量名.[index]
返回某一项。
3.迭代器:
begin()
: 返回指向向量第一个元素的迭代器。end()
: 返回指向向量尾部的下一个位置的迭代器。
深层复制与浅层复制
浅层复制
实现对象间数据元素的一一对应复制。
深层复制
当被复制的对象数据成员是指针类型时,不是复制该指针成员本身,而是将指针所指对象进行复制。
浅层复制
按照书上例6-21的写法,如下,但是程序崩溃,一直输出“destrcutor called
”,
#include <iostream>
using namespace std;class Point{
public:Point():x(0),y(0){cout << "default constructor called" << endl;}Point(int x,int y):x(x),y(y){cout << "constructor called" << endl;}~Point(){cout << "destrcutor called" << endl;}int getX(){return x;}int getY(){return y;}void move(int newX,int newY){x = newX;y = newY;cout << "move to x : " << x << ", y : " << y << endl;}
private:int x,y;
};
class ArrayOfPoint{
public:ArrayOfPoint(int size):size(size){points = new Point[size];}~ArrayOfPoint(){cout << "deleting" << endl;delete[] points;}Point& Element(int index){assert(index >= 0 && index <size);return points[index];}
private:Point *points;int size;
};int main(){int n;cout << "n=" ;cin >> n;ArrayOfPoint array1(n);array1.Element(0).move(10, 15);array1.Element(1).move(20, 25);//浅层复制ArrayOfPoint array2 = array1;cout << "copy array1...." << endl;cout << "point_0 from array2, x = " << array2.Element(0).getX() << " y = " << array2.Element(0).getY() << endl;cout << "point_1 from array2, x = " << array2.Element(1).getX() << " y = " << array2.Element(1).getY() << endl;//修改array1cout << "moving array1 ......" << endl;array1.Element(0).move(100, 105);array1.Element(0).move(200, 205);//输出array2cout << "after moving array1...." << endl;cout << "point_0 from array2, x = " << array2.Element(0).getX() << " y = " << array2.Element(0).getY() << endl;cout << "point_1 from array2, x = " << array2.Element(1).getX() << " y = " << array2.Element(1).getY() << endl;return 0;
}
(有些电脑可能不会崩溃…)
为什么会这样?因为浅层拷贝会导致两个ArrayOfPoint
类型的变量会导致指向同一片内存区域;所以在程序结束时,会调用array1
的析构函数,销毁array1指向的内存,但是到了调用array2的析构函数的时候,也会再一次销毁,但是这个时候这片内存已经在之前销毁了;即,两个对象共享了同一片内存区域,但是销毁了2次,当然会导致运行错误。
深层拷贝
完整代码如下:
#include <iostream>
using namespace std;class Point{
public:Point():x(0),y(0){cout << "default constructor called" << endl;}Point(int x,int y):x(x),y(y){cout << "constructor called" << endl;}~Point(){cout << "destrcutor called" << endl;}int getX(){return x;}int getY(){return y;}void move(int newX,int newY){x = newX;y = newY;cout << "move to x : " << x << ", y : " << y << endl;}
private:int x,y;
};
class ArrayOfPoint{
public:ArrayOfPoint(int size):size(size){points = new Point[size];}ArrayOfPoint(const ArrayOfPoint& v);~ArrayOfPoint(){cout << "deleting" << endl;delete[] points;}Point& Element(int index){assert(index >= 0 && index < size);return points[index];}
private:Point *points;int size;
};
ArrayOfPoint::ArrayOfPoint(const ArrayOfPoint& v ){size = v.size;points = new Point[size];for(int i = 0;i < size;i++){points[i] = v.points[i];}
}int main(){int n;cout << "n=" ;cin >> n;ArrayOfPoint array1(n);array1.Element(0).move(10, 15);array1.Element(1).move(20, 25);//浅层复制ArrayOfPoint array2 = array1;cout << "copy array1 done...." << endl;cout << "point_0 from array2, x = " << array2.Element(0).getX() << " y = " << array2.Element(0).getY() << endl;cout << "point_1 from array2, x = " << array2.Element(1).getX() << " y = " << array2.Element(1).getY() << endl;//修改array1cout << "moving array1 ......" << endl;array1.Element(0).move(100, 105);array1.Element(0).move(200, 205);//输出array2cout << "after moving array1...." << endl;cout << "point_0 from array2, x = " << array2.Element(0).getX() << " y = " << array2.Element(0).getY() << endl;cout << "point_1 from array2, x = " << array2.Element(1).getX() << " y = " << array2.Element(1).getY() << endl;return 0;
}
执行结果:
n=2
default constructor called
default constructor called
move to x : 10, y : 15
move to x : 20, y : 25
default constructor called
default constructor called
copy array1 done....
point_0 from array2, x = 10 y = 15
point_1 from array2, x = 20 y = 25
moving array1 ......
move to x : 100, y : 105
move to x : 200, y : 205
after moving array1....
point_0 from array2, x = 10 y = 15
point_1 from array2, x = 20 y = 25
deleting
destrcutor called
destrcutor called
deleting
destrcutor called
destrcutor called
Program ended with exit code: 0
可以看到,移动了array1
之后,array2
的x和y还是没有变,因为这两个对象已经指向了不同的2个内存区域。并且在程序结束的时候,调用了2次析构函数,分别释放2个对象的内存区域,不再引起程序错误。
字符串
字符串常量
例:“program”
各字符连续、顺序存放,每个字符占一个字节,以‘\0’结尾,相当于一个隐含创建的字符常量数组
“program”出现在表达式中,表示这一char数组的首地址
首地址可以赋给char常量指针:
const char *STRING1 = "program";
注:因为是常量,所以需要加上const
.
字符串变量
和字符串常量存储一样,只不过程序员自己创建的,可以改变其内容
用字符数组存储字符串(C风格字符串)
用字符数组表示字符串的缺点:
-
执行连接、拷贝、比较等操作,都需要显式调用库函数,很麻烦
-
当字符串长度很不确定时,需要用new动态创建字符数组,最后要用delete释放,很繁琐
-
字符串实际长度大于为它分配的空间时,会产生数组下标越界的错误
-
不符合面向对象的要求(解释如下)
为什么在C++中,用数组来存放字符串会导致数据与处理数据的函数分离?(数据与处理数据的函数分离是不符合面向对象方法的要求的)
解释:
在C++中,使用字符数组来存放字符串会导致数据与处理数据的函数分离的主要原因是字符数组本身是一种比较原始的数据结构,它仅仅是一系列连续的字符,而不提供对字符串的高级操作和管理功能。这种设计方式导致了数据(字符串)与处理数据的函数之间的耦合度较低,因为处理数据的函数需要直接操作字符数组,而不是通过更高级别的抽象来管理字符串数据。
具体来说,使用字符数组存放字符串会导致以下问题:
缺乏封装性: 字符数组本身没有封装性,它只是一块内存,不提供任何关于字符串的操作函数。因此,处理字符串的函数需要直接操作数组元素,无法通过字符串对象的方法来完成。
容易引发错误: 直接操作字符数组容易引发越界访问和内存泄漏等问题。由于字符数组没有边界检查机制,因此处理数据的函数必须自行确保数组边界的有效性,这增加了程序员编写代码的负担,并且容易引入错误。
不利于维护和重用: 将数据与处理数据的函数分离会导致代码的可维护性和重用性降低。如果需要修改字符串的表示方式或者处理逻辑,需要修改多处直接操作字符数组的代码,这增加了代码维护的难度。
不符合面向对象方法的要求: 面向对象编程强调数据和操作数据的行为应该封装在一起,形成一个类。而直接操作字符数组的方式与这种思想相悖,导致了数据与处理数据的函数分离,不符合面向对象方法的要求。
因此,为了提高代码的可维护性、可读性和可重用性,以及符合面向对象方法的设计原则,推荐使用
std::string
类来代替字符数组来表示和处理字符串。std::string
类封装了字符串的数据和操作方法,提供了更高级别的抽象和功能,能够更好地解决上述问题。
string类
使用字符串类string表示字符串
注:本身string是类模版
basic_string
的一个特殊化实例,并非一个单独的类。但是因为对于使用者来说,string的特点跟一个独立的类没有什么区别,可以把它当作一个单独的类来看待。
string实际上是对字符数组操作的封装
string类常用的构造函数
string(); //默认构造函数,建立一个长度为0的串
例:
string s1;string(const char *s); //用指针s所指向的字符串常量初始化string对象
例:
string s2 = “abc”; string(const string& rhs); //复制构造函数
例:
string s3 = s2;
string类常用操作
s + t 将串s和t连接成一个新串
s = t 用t更新s
s == t 判断s与t是否相等
s != t 判断s与t是否不等
s < t 判断s是否小于t(按字典顺序比较)
s <= t 判断s是否小于或等于t (按字典顺序比较)
s > t 判断s是否大于t (按字典顺序比较)
s >= t 判断s是否大于或等于t (按字典顺序比较)
s[i] 访问串中下标为i的字符
例:
string s1 = "abc", s2 = "def";
string s3 = s1 + s2; //结果是"abcdef"
bool s4 = (s1 < s2); //结果是true
char s5 = s2[1]; //结果是'e'
注:之所以能够使用上面的这些运算符是因为string类对这些操作符进行了重载。
例6-23 string类应用举例
#include <string>
#include <iostream>
using namespace std;//根据value的值输出true或false
//title为提示文字
inline void test(const char *title, bool value)
{cout << title << " returns " << (value ? "true" : "false") << endl;
}int main() {string s1 = "DEF";cout << "s1 is " << s1 << endl;string s2;cout << "Please enter s2: ";cin >> s2;cout << "length of s2: " << s2.length() << endl;//比较运算符的测试test("s1 <= \"ABC\"", s1 <= "ABC"); test("\"DEF\" <= s1", "DEF" <= s1);//连接运算符的测试s2 += s1;cout << "s2 = s2 + s1: " << s2 << endl;cout << "length of s2: " << s2.length() << endl;return 0;
}
思考:如何输入整行字符串?
用cin的>>
操作符输入字符串,会以空格作为分隔符,空格后的内容会在下一回输入时被读取
getline
可以输入整行字符串(要包括string头文件),例如:
getline(cin, s2);
输入字符串时,可以使用其它分隔符作为字符串结束的标志(例如逗号、分号),将分隔符作为getline的第3个参数即可,例如:
getline(cin, s2, ',');
注意:
例6-24 用getline输入字符串
include <iostream>
#include <string>
using namespace std;
int main() {for (int i = 0; i < 2; i++){string city, state;getline(cin, city, ',');getline(cin, state);cout << "City:" << city << “ State:" << state << endl;}return 0;
}
执行:
Beijing,China
City:Beijing State:China
San Francisco,the United States
City:San Francisco State:the United States
Program ended with exit code: 0