一、数组
- 数组是具有一定顺序关系的若干对象的集合体,组成数组的对象称为该数组的元素。
- 每个元素有n个下标的数组称为n维数组。
1、数组的声明与使用
1)数组的声明
数据类型 标识符 [常量表达式1] [常量表达式2]......
2)数组的使用
数组名 [下标表达式1] [下标表达式2]......
- 不能越界!
2、数组的储存与初始化
1)数组的储存
数组元素在内存中是顺序、连续存储的。
- C++二维数组会按行优先的顺序存储。
2)数组初始化
数组的初始化就是在声明数组时给部分或全部元素赋初值。
//初始化
int a[3] = {1,1,1};
//可以只部分元素初始化,剩下的默认为0
float fa[5] = {1.0,2.0,3.0}
//二维数组,下面三个等价
int b[2][3] = {1,0,0,0,1,0};
int b1[][3] = {1,0,0,0,1,0};
int b3[2][3] = {{1,0,0},{0,1,0}};
//常量数组
const float fa[5] = {1.0,2.0,3.0,4.0,5.0}
3)数组作为函数参数
使用数组名传递数据时,传递的是地址。如果使用数组名做函数的参数,则实参和形参都应该是数组名,类型要相同。
void f(int a[][4])
{
}
int main(){
int a[3][4] = {1,2,3,4,5,6,7,8,9,0,1};
f(a);//用函数名调用函数
return 0;
}
4)对象数组
对象数组语句形式:
类名 数组名[常量表达式];
可以通过数组名和下标直接访问到公有成员:
数组名[下标表达式].成员名;
也可以进行初始化:
class A{
public:
A(int a):x(a);//有参构造
A(){};//默认构造
int x;
};
int main(){
A array[3] = {A(1),A(2)};//初始化数组,剩下的会使用默认构造函数初始化
return 0;
}
二、指针
1、指针变量的声明
指针变量是用于存放内存单元地址的。
声明指针语法:数据类型 *标识符;
指针变量声明时要指出数据类型有两个原因:1、声明了变量所需要的内存空间。2、规定了该指针可以参加算数运算、关系运算等运算以及相应的运算规则。
2、* 和 & 运算符
“ * ” 称为指针运算符,也称解析,表示获取指针所指向的变量的值。
“ & ” 称为取地址运算符,用来得到一个对象的地址。这两个运算符都是一元运算符。 这两个运算符出现在声明语句和执行语句的含义是不一样的:
int *p; //声明p是一个int型指针
cout << *p; //输出指针p所指向的内容
int &rf; //声明一个int型的引用rf
int a, b;
int* pa = &b; //& 表示取地址
pa = &b; //& 表示取地址
3、指针的赋值
两种方法:
//在定义指针的同时进行初始化赋值:
存储类型 数据类型 *指针名 = 初始地址;
int a = 1;
int* b = &a;//定义并初始化
//在定义之后,单独使用赋值语句:
指针名 = 地址
int c[10];
int* d;
d = c;//数组名称实际上就是一个不能被赋值的指针,表示该数值的起始地址
- 指向常量的指针(常量指针);不能通过指针来改变所指对象的值,但指针本身可以改变,可以指向另外的对象。
- 指针类型的常量(指针常量);指针本身的值不能改。
//常量指针
int a;
const int *p1 = &a;
int b;
p1 = &b; //正确
*p1 = 1; //编译时错误,不能通过p1来改变所指对象的值。
//指针常量
int* const p2 = &a;
p2 = &b; //错误,指针常量所指向的地址无法改变
4、指针运算
指针可以和整数进行加减运算。
假设有指针 p1 和整数 n1,则
- p1+n1 表示指针 p1 当前所指位置后方第 n1 个数的地址。
- p1-n1 表示指针 p1 当前所指位置前方第 n1 个数的地址。
- 指针++ 或 指针-- 表示指针当前所指位置下一个或前一个数据的地址。
- *(p1+n1)表示指针 p1 当前所指位置后方第 n1 个数的内容,等价于 p1[n1] ,同理 *(p1-n1) 也等价于 p1[-n1]
一般来说指针的算术运算是和数组的使用相联系的,因为只有在使用数组时,才会得到连续分布的可操作内存空间。
空指针,也就是一个不指向任何有效地址的指针。
int *p;
p = 0; //将p设为空指针
int *p = NULL; //空指针也可以用NULL来表示
5、指针处理数组元素
直接看例子:
#include<iostream>
using namespace std;
void main()
{
int a[5] = { 1,2,3,4,5 };
//通过指针的方式遍历数组
for (int* p = a; p < (a + 5); p++) {
cout << *p << endl;
}
system("pause");
return ;
}
上面这个例子通过计算后得到尾指针,但很容易出错。C++11引入了两个标准库函数begin和end,两个函数将数组作为他们的参数。两个函数定义在iterator头文件中。
- begin函数可以返回数组的首元素的指针;
- end函数返回指向数组尾元素的下一位置的指针;
#include<iostream>
#include<iterator>
using namespace std;
void main()
{
int a[5] = { 1,2,3,4,5 };
//pbeg指向a的首元素,pend指向尾元素的下一位置
int* pbeg = begin(a), * pend = end(a);
//遍历数组
while (pbeg != pend)
{
cout << *pbeg << endl;
pbeg++;
}
system("pause");
return ;
}
6、指针数组
如果一个数组的每个元素都是指针变量,这个数组就是指针数组。指针数组的每个元素都必须是同一类型的数组。声明语法:
数据类型 *数组名[下标表达式];
具体使用:
#include<iostream>
#include<iterator>
using namespace std;
void main()
{
int a[5] = { 1,2,3,4,5 };
int b[5] = { 1,0,0,0,0 };
int c[5] = { 0,0,0,0,5 };
//声明一个指针数组,并初始化。
int* pa[3] = { a,b,c };
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 5; j++) {
cout << pa[i][j]; //pa[i][j] 等价于 *(pa[i] + j)
}
cout << endl;
}
//输出结果如下:
//12345
//10000
//00005
system("pause");
return ;
}
指针数组和二维数组有相同的访问形式[][],但具体的访问过程不太一样以 a[ i ][ j ] 举例。
- 如果a是一个指针数组,则 a[ i ] 的值要先访问a的第i个元素才能获得,任何再获得 a[ i ] 作为首地址的数组的第 j 个元素。
- 如果 a 是一个二维数组,a[ i ] 可以直接通过二维数组的首地址计算得到,内存里面并没有一个指针数组老存储a[ i ]的值。二维数组在内存中是以行优先的方式按照一维顺序关系存放的。所有的内存都是连续的。
我们也可以把二维数组当作指针数组来访问。
*(*(array + i) + j) //表示的就是二维数组 arrary的第i行j列的元素 array[i][j]
7、指针作为函数参数
#include<iostream>
#include<iterator>
using namespace std;
void f(int* a){ //指针作为函数参数
*a = 10;
}
void main()
{
int a = 0;
f(&a);
cout << a << endl;//输出结果为10
system("pause");
return ;
}
如果函数体中不需要通过指针改变指针所指向对象的内容,应在参数表中将其声明为指向常量的指针,这样使得常对象被取地址后也可作为该函数的参数。
8、指针型函数
当一个函数的返回值是指针类型时,这个函数就是指针型函数。指针型函数一般定义形式:
数据类型 *函数名(参数表)
{
函数体
}
//也可以用decltype关键字声明返回类型
int a[] = {0,1,2,3,4};
int b[] = {0,1,2,3,4};
decltype(a) *fun(int i){ //返回一个指针指向含有5个整数的数组。
return b;
}
函数返回数组指针:
类型说明符 (*函数名(参数表))[数组维度]
//eg:
int (*fun(int i))[10] //返回一个指向含有10个整数的数组的指针。
//C++11可以使用尾置返回类型
auto fun(int i) -> int (*)[10];
//也可以使用typedef
typedef int arr[10]; //arr是一个类型别名,含有10个整数的数组
using arr = int[10]; //arr的等价说明
arr * fun(int i); //返回一个指向含有10个整数的数组的指针
9、指向函数的指针
函数名就表示函数的代码在内存中的起始地址,调用函数的通常形式“函数名(参数表)”的实质就是“函数代码首地址(参数表)”。
函数指针就是专门用来存放函数代码首地址的变量,(C#的委托就是基于这个来的)
函数指针的定义:
数据类型 (*函数指针名)(形参表);
//eg:
void (*func)(float);
//上面的方式较为繁琐,我们也可以使用typedef
typedef int(* DoubleIntFunction)(double);//声明了DoubleIntFunction为 有一个double形参、返回类型为int的指针 类型的别名
DoubleIntFunction funcPtr;//声明了一个具有该类型的函数指针。
可以通过函数指针调用函数
#include<iostream>
#include<iterator>
using namespace std;
void fun (float m)
{
cout << m;
}
void main()
{
void (* funcPoint)(float); //函数指针
funcPoint = fun; //函数指针指向fun
funcPoint(1.2f); //函数指针调用
system("pause");
return ;
}
10、对象指针
1)对象指针
对象指针声明:
类名 *对象指针名;
//eg:
class A{};
A *aP //声明一个对象指针
使用对象指针也可以方便的访问对象的成员:
对象名->成员名
2)this指针
this指针是一个隐含于每一个类的非静态成员函数中的特殊指针(包括构造函数和析构函数),它用于指向正在被成员函数操作的对象。
this是一个指针常量,对于常成员还书,this同时又是一个指向常量的指针。在成员函数中,可以使用*this来标识正在调用该函数的对象。
当局部作用域中声明了于类成员同名的标识符时,对该标识符的直接引用代表的是局部作用域中所声明的标识符,这是为了访问该类成员,就可以通过this指针。
3)指向类的非静态成员的指针
语法:
//声明指针:
类型说明符 类名::*指针名; //声明指向数据成员的指针
类型说明符 (类名::*指针名)(参数表); //声明指向函数成员的指针
//给指针赋值:
指针名 = &类名::数据成员名; //数据成员指针赋值
指针名 = &类名:函数成员名; //函数指针赋值
//经过上面的赋值后,只是说明了被赋值的指针是专门用于指向哪个数据成员的,
//同时在指针中存放该数据成员在类中的相对位置(相对于起始位置的偏移)
//访问指针:
对象名.*类成员指针名
或
对象名->*类成员指针名
//函数指针调用成员函数:
(对象名.*类成员指针名)(参数表)
(对象名->*类成员指针名)(参数表)
举例:
#include <iostream>
using namespace std;
class Point { //类的定义
public: //外部接口
Point(int x = 0, int y = 0) : x(x), y(y) {} //构造函数
int getX() const { return x; } //返回x
int getY() const { return y; } //返回y
private: //私有数据
int x, y;
};
int main() { //主函数
Point a(4, 5); //定义对象A
Point *p1 = &a; //定义对象指针并初始化
int (Point::*funcPtr)() const = &Point::getX; //定义成员函数指针并初始化
cout << (a.*funcPtr)() << endl; //(1)使用成员函数指针和对象名访问成员函数
cout << (p1->*funcPtr)() << endl; //(2)使用成员函数指针和对象指针访问成员函数
cout << a.getX() << endl; //(3)使用对象名访问成员函数
cout << p1->getX() << endl; //(4)使用对象指针访问成员函数
return 0;
}
4)指向类的静态成员的指针
类的静态成员的访问是不依赖于对象的,因此可以用普通的指针来指向和访问静态成员。
#include <iostream>
using namespace std;
class A {
public:
static int m;
static void func();
};
void A::func() {
cout << "func" << endl;
}
int A::m = 10; //静态数据成员必须在类外定义和初始化
int main() { //主函数
int* ptr = &A::m; //定义一个int型指针,指向类的静态成员
void (*funcPtr)() = A::func; //定义一个指向函数的指针,指向类的静态成员函数
cout << *ptr << endl; //通过指针输出m
funcPtr(); //通过函数指针调用函数
return 0;
}
三、动态内存分配——new、delete
new运算符的功能是动态分配内存,或者称为动态创建堆对象。
- new语句运行过程中申请分配用于存放指定类型数据的内存空间,并根据初始化参数列表中给出的值进行初始化。
- 如果内存申请成功,就会返回一个指向新分配内存首地址的类型的指针。
new 数据类型(初始化参数列表);
new 类型名[数组长度]; //new也可以创建数组类型的对象
new 类型名[数组第一维度长度][第二维度长度]...//多维数组
int *a = new int(2); //分配了int类型数据的内存空间,并将初值2存入该空间中
int *b = nwe int; //表示没有设定初值
int *c = new int(); //默认用0初始这个int
delete运算符用来删除new建立的对象,释放指针所指向的内存空间。
- 如果被删除的是对象,则析构函数会被调用。
- 对于new建立的对象只能delete一次。
- new分配的内存,必须用delete加以释放,否则会导致动态分配的内存无法回收,使得程序占据的内存越来越大,这称为“内存泄漏”。
- 删除之后还可以继续给那个指针赋值。
delete 指针名;
delete []指针名; //删除new建立的数组
calss A;
A *ptr = new A;
delete ptr;
ptr = new A; //可以重新赋值,分配内存
A *p = new A[2];//创建对象数组
delete[] p; //删除整个对象数组
四、用vector创建数组对象
向量(Vector)是一个封装了动态大小数组的顺序容器(Sequence Container)。跟任意其它类型容器一样,它能够存放各种类型的对象。可以简单的认为,向量是一个能够存放任意类型的动态数组。定义如下:
vector<元素类型>数组对象名(数组长度);
//eg:
vector<int>arr(10);//定义了一个大小为10的int型动态数组对象arr
vector定义的数组对象的所有元素都会被初始化,如果是基本数据类型,就会用0来初始化。如果是类对象,则会调用类的默认构造函数。所以要保证类具有默认构造函数。
vector内置了很多函数,下面是部分函数:
- int size() const:返回向量中元素的个数
- void push_back(const T& x):向量尾部增加一个元素X
- iterator erase(iterator it):删除向量中迭代器指向元素
- void pop_back():删除向量中最后一个元素
- void clear():清空向量中所有元素
五、深层复制与浅层复制
浅层复制:实现对象间数据元素的一一对应复制。
深层复制:当被复制的对象数据成员是指针类型时,不是复制该指针成员本身,而是将指针所指对象进行复制。
深层复制与浅层复制最主要的区别在于:
- 浅层复制只是对指针的复制,两个对象中的指针实际上指向同一块内存区域,所以改变其中任意一个的值,都会对另一个产生影响;特别是程序结束之前会调用析构函数,很可能会对同一块内存空间释放两次,导致运行错误。
- 深层复制是对元素值的复制,复制构造函数会重新申请一块大小合适的内存空间来存储原来元素的值,所以深层复制后的对象与原对象之间并无关联关系。
所以当类中有数组(指针)时,用浅层复制就容易出错。这时候就要我们编写复制构造函数。
六、字符串
1、用字符数组存储和处理字符串
字符串常量是用一对双引号括起来的字符序列。在内存中的存放形式是,按顺序,每个字符占1字节,并在末尾添加 '\0' 作为结尾标志。这实际上是一个隐含创建的类型为char的数组,一个字符串常量就能表示这样一个数组的首地址。eg:
const char* charArr = "Hello World!";
cout << charArr << endl; //输出结果就是 Hello World!
字符串变量也可以用类似的方式来表示。如果创建一个char数组,每个元素存放字符串的一个字符,在末尾放置一个 '\0' ,就构成了字符串。
//大括号字符个数小于字符数组长度,其余空元素会自动赋值为空字符
char str1[10] = { 'a', 'b', 'c' };
cout << sizeof(str1) << endl; //输出为10
//大括号字符个数等于字符数组长度,系统不会自动添加'\0',
char str2[3] = { 'a', 'b', 'c' };
cout << sizeof(str2) << endl; //输出为3
//这种以字符串的形式赋值的,系统会自动在最后面添加'\0'
char str3[] = "hello world!";
cout << sizeof(str3) << endl; //输出为13,最后还有一个我们看不见的'\0'
cout << strlen(str3) << endl; //输出为12,这个函数返回字符串长度有效长度,碰到第一个‘\0’就会停止计算
//下面这种情况会报错,会提示const char[13] 不能赋值给 const char[12]
//因为字符串最后都会有一个'\0',把12改成13编译才能通过
char str[12] = "hello world!";
2、string类
(严格来说string是类模板basic_string的严格特殊化实例)
1)构造函数的原型
string(); //默认构造函数,建立一个长度为0的串
string(const string &rhs); //复制构造函数
string(const char *s); //用指针s指向的字符串常量初始化string类的对象
string(const string& rhs, unsigned int pos, unsigned int n);
//将对象rhs中的串从pos开始取n个字符用来初始化
string(const char* s,unsigned int n);//指针s所指向的字符串中的前n个字符用来初始化
string(unsigned int n,char c);//将参数c中的字符重复n次,用来初始化string类的对象
2)string类的操作符
string对操作符进行了重载,所以可以进行下面这些运算。
== // 关系操作符,从两个字符串的首字符根据字符的ASCII值开始比较
>
<
>=
<=
!=
+ //s1 + s2,把 s1 和 s2 连接成一个新字符串, 返回新生成的字符串
+= // s1 += s2, 把 s2追加到s1中
[] // str[n], 返回 str 中位置为 n 的字符,位置从 0 开始计数
3)常用成员函数
string append(const char* s);
//将字符串s添加在本串尾部
string assign(const char* s);
//赋值,将s所指向的字符串赋值给本对象
int compare(const string &str) const;
//比较本串和str串的大小,本串大返回正数,本串小返回负数,相等返回0
string &insert(unsigned int p0,const char *s);
//将s字符串插入在本串位置p0之前
string substr(unsigned int pos, unsigned int n) const;
//取子串,取本串位置pos开始的n个字符,构成新的string返回
unsigned int length() const;
//返回串的长度(字符个数)
void swap(string &str);
//将本串与str中的字符串进行交换
4)getline()函数
如果直接使用cin的>>操作符从键盘输入字符串,空格会被作为输入的分隔符。
为了避免这种情况发生我们可以使用getline函数(在string头文件中),这个函数可以读到行末为止(也就是回车键作为分隔符),也可以自定义分隔符
string s;
getline(cin,s); //从键盘输入一个字符串s
getline(cin,s,',');//用逗号作为分隔符