【4】C++进阶系列(数组和指针)

毫无疑问,数组和指针是C和C++里面的核心。字字句句都要仔细斟酌

导学:

1、数组的引出-:

问题1:假如变量很多,不能能给每一个变量去取变量名,这时候数组是一个很好的选择,数组是一种构造类型,用于存放相同或者是相似类型的数据或者对象,并且可以以顺序的方式来访问。

2、指针的引出-:

c++是完全兼容c的,而c是用来写操作系统的,它可以通过地址的方式来访问内存空间。但只能用合法获得的地址,访问程序允许我们程序访问的内存空间。

指针是一个用来存放地址的数据类型。可以用指针来存放数据的地址,也可以存放程序代码(函数)的地址,所以可以通过地址去访问数据以及调用函数

问题2:函数有函数名,变量有变量名,干嘛非得用地址去访问?而且还不好用

——因为有上述问题,所以大多数时候,能不用指针尽量不用指针。可是有的时候,不得不使用指针。比如:动态内存分配。动态分配的内存,获得的返回值只能是指针类型,这时就不得不使用指针来进行数据访问了:因为没有变量名,所以只能用指针来访问。

——另外,使用数组的时候有点麻烦,可能会出现越界的情况。这时类库提供了vector的方法,是面向对象的一种数组。可以灵活的改变长度,只需要在越界的时候对数组长度进行伸缩就可以了。另外,相比于传统数组,它也可以直接通过下标来访问。

问题2:在类中定义某些成员是指针,在运行时,当需要内存空间的时候,我们去做动态内存的分配,把返回的地址赋到成员变量指针里面去,这时假如要做复制构造,会发生什么问题呢?——指针指向某一个运行时动态分配的存储空间,用这样一个已有对象去初始化一个新的对象,那既然是复制,肯定新对象也需要分配一个存储空间。这个时候,浅层复制就无法做到,就引入了深层复制(深拷贝)的概念。用于申请内存空间

问题3:字符串常量的概念和字符串变量的概念。因为:整形,浮点型都有常量和变量的说法,也有字符串常量的说法,但是很少有说字符串变量的。双引号“”来表示字符串常量,没有字符串变量的话有吧常量放在哪里呢?——一种是从c中继承过来的传统方式:用字符数组来存储和处理字符串,但是就会存在越界等问题,以及末尾要有\0作为结束的标志,忘记这个标记的话就不是字符串了。第二种方法:String类——可以把字符串用面向对象的方式处理,方便的进行字符串的比较拼接等操作。它比数组来存放的方式更加安全和方便


数组:是具有一定顺序关系的若干相同类型变量的几何体,组成数组的变量称为该数组的元素。

定义:

                                            

数组需要先定义在引用。

依次使用同一种算法进行大批量运算。注意这个“次序”的理解,此前的例子,次序都体现在数据本身,比如等差数列,比如1+2+……+100的和,元素和元素之间有顺序关系。但多数情况下,数据的值本身是没有这种有规律的次序关系的。这个时候就需要数组来进行处理,用下标来强加一种次序。这样的话,循环语句和下标配合使用就能依次处理大批量的数据了。

3、数组的存储和初始化

一维数组的存储:数组元素在内存中顺次存放,他们的地址是连续的。元素之间的物理地址上的相邻,对应着逻辑次序上的相邻。

                                               

数组名字是数组首元素的内存地址。数组名是一个地址类型的常量(指针),不能被赋值。地址类型就是指针。所以不能对数组名进行赋值,因为它是常量,不可改变的。

数组的初始化方式:1、列出全部的元素初始值。如:static int a[10]={0,1,2,3,4,5,6,7,8,9};

2、给出一部分元素的初始值:static int a[10]={0,1,2,3,4};

3、列出全部的初始值但是不指定数组的长度。static int a[]={0,1,2,3,4,5,6,7,8,9};编译器会按照给出的初始值的元素个数去确定数组元素的个数。

二维数组的存储:二维数组理解为由一位数组组成的数组。二位数组的存储方式是按照行存储

                                                             

二位数组的初始化:1、列出全部的初始值;2、分组;3、部分初始化;4、全部初始值,可以省略第一个下标。

                                                        

但是要注意的是,必须连续的初始化。不能跳跃,比如给第1,3,5个元素初始化。

数组不介意初始值,因为时候一般马上回从键盘或者文件中读入数据,或者是用于存放运行结果之类的数据。所以有时会定义数组但是不会给它初始化。如果不给它初始化,局部作用域的非静态数组中会存放垃圾数据(是不可靠的),static数组中的数据初始化为0。如果只对部分元素初始化,剩下的未显式初始化的元素会自动初始化为0。

例子:数组存放斐波那契数列

#include<iostream>

using namespace std;

int main() {
	int f[20] = {1,1 };//初始化第0、1个数
	for (int i = 2; i < 20; i++){//求第2~19个数
		f[i] = f[i - 2] + f[i - 1];
	}
		for (int i = 0; i < 20; i++) {//输出,每行5个数
			if (i % 5 == 0)
				cout << endl;
				cout.width(12);//设置输出宽度为12
				cout << f[i];
		}
	return 0;
}

例子:一位数组的应用

#include<iostream>
using namespace std;

int main() {
	const char a[5] = { 'a','b','c','d','a' };
	char b;
	float accuracy=0;
	int num = 0;
	int ans = 0;
	cout << "连续输入5个答案:" << endl;
	while (cin.get(b))
	{
		if (b != '\n') 
		{
			if(b==a[ans]){
				num++; cout << " ";
			}
			else {                     //最好还是加{}的方式,如果不使用{},切记是执行else后面紧接着的一句
				cout << "*";
			}
				ans++;//?
		}else{
			accuracy = num / 5.0;
			cout << "The Score is:" << static_cast<float>(accuracy * 100) << '%' << endl;
			ans = 0;
			num = 0;
			cout << endl;
			}
		}	
	return 0;
}

4、数组作为函数参数

数组元素做实参:与单个变量一样;

数组名做实参:形、实参数都应是数组名,类型要一样。传递的是数组的首地址。因为传递的是地址那么对形参数组的改变会直接影响到实参数组。这时可能会要求考虑原数组的类型,是不双向传递的常数组还是双向传递的数组。

5、对象数组

数组是有一定数据关系的,同类型的数据的集合。对象是属于自定义类的实例。所以,可以由一个个的对象来构成一个数组。

定义:类名 数组名[元素个数]

访问成员:通过下标访问,因为元素是对象所以需要用.号引用对象成员,数组名[下标].成员名。初始化对象数组,就是初始化对象,就要调用构造函数,数组中每一个元素对象被构建时,系统都会调用类的构造函数初始化该对象。初始化几次对象数组,就要调用几次构造函数。如果不给初始值,则调用默认构造函数

                                            

注意是通过初始化列表进行赋值。

                                         

例子:定义使用对象数组

                                

//test.cpp
#pragma once
#include<iostream>
#include"Point.h"
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;
}

#pragma once
//Point.h
#ifndef _POINT_H_
#define _POINT_H_


class Point
{
public:
	Point();
	Point(int x, int y);
	~Point();
	void move(int newX, 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 << "Deconstructoe called." << endl;
}

void Point::move(int newX,int newY) {
	cout << "Moving the Point to ( " << newX <<","<< newY << ")" << endl;
	x = newX;
	y = newY;
}

6、基于适用范围的for循环:

和传统的for循环区别在于处理数组是不需要知道数组的长度。处理一个容器的时候不用控制循环次数。可以自动的遍历一遍整个容器。具体看下图,左边是传统for;右边是范围for:(在处理容器时,使用基于范围的for循环更方便)

                                     

7、指针

问题:当需要访问内存空间的时候要怎么样访问呢?

定义一个变量就是讲这个变量的变量名和若干字节的内存空间结合起来。

内存空间的访问方式:1、通过变量名来访问;2、通过内存空间的地址来访问(从c语言继承来的)

指针:一个地址类型的变量(内存地址),用于间接访问内存单元。指针就是地址

指针变量:用于存放地址的变量。

问题:声明指针就好了,为什么还要有类型int*?

因为要说明指针指向的对象是什么类型。以后是需要通过指针访问对象的。如果不知道对象是什么类型,当拿到起始单元的地址之后,当要从地址中中去多少个字节呢?不知道类型就不知道内容占的字节数,就不知道要取几个字节的内容

                                      

定义好的指针怎样使用?int* ptr=&i,这里的*表明ptr是指针类型,此时如果想要给i赋值为3:*ptr=3;这里的*作为一元运算符,表示指针运算。指针运算实际上是一个寻址的过程,表示以ptr为地址去寻找这个地址所指向的目的地,找到之后将3赋值到这个目的地。如图,就复制到了地址为2000的这个目的地中了。赋值进去让它占用几个字节呢?这个时候就要看定义ptr的时候将他定义为了什么类型的指针,即赋值进去它会占到int类型的字节数。如下图:

                                               

*ptr=3;具体表示如下:ptr里面是放的是一个地址,拿着这个地址去找到它的目标对象,找到了就完成了*ptr这个指针运算,然后再是赋值运算。这件就是指针变量的使用过程。

                                  

与指针(地址)运算相关的运算——“*”和“&”

指针运算*——实际上是一个寻址过程。以指针变量李存放的内容作为地址,按照这个地址去找到(访问)地址对应的内存单元:这个过程是指针运算的过程,实际上就是寻址的过程。

地址运算“&”——实际上是去计算一段代码或者变量的地址,把他的地址反馈回来。也就是说地址运算的结果返回的是内存变量或者对象的地址。

指针运算(*)和地址运算(&)互为逆运算。

8、指针的初始化和赋值

定义变量的时候最好是要做初始化,否则可能有垃圾数据,盲目的投入计算可能会带来不可预见的错误。当定义指针变量的时候,如果给指针变量初始化,也能避免其访问内存空间的时候出现一些意外的错误。

那么,指针变量该如何初始化呢?可不可以用一个一般的整数或某一个计算的结果就给指针变量赋值?就把地址放在里面呢?——不能:指针变量中存放的只能是我们程序中合法获得的地址。

初始化:这里的a应该是之前定义过的变量。变量a要在定义指针之前声明过,而且指针所指向的类型要和变量的类型一样。

                                                

不能用内部的非静态变量去初始化一个static指针。

赋值:赋给指针变量的值必须是地址常量或者变量,不能是普通整数。

                                                

那么:什么是合法获得的地址呢?——合法地址比如:通过取地址运算“&”获得的已经定义过的变量和对象的起始地址。或者是动态内存分配操作成功时返回的地址,这样的值可以赋值给指针。

但是,整数0是例外,0可以赋值给指针。它并不表示让指针指向地址为0的空间,而是表示这个指针是空指针。

允许定义或者声明指向void类型的指针,该指针可以被赋予任何类型的对象的地址,而不能用它来访问它所指向的内存空间。比如void *general;

另外,c++11中使用nullptr关键字表示空指针,使得表达更加准确和安全。传统的是使用null

指针定义和初始化例子如下:注意说法pv = &i——void类型指针指向int类型的变量i。另外可以static_cast<int *>来转换。

                                                  

区别指向常量的指针和常量指针。

指向常量的指针:

不能通过指向常量的指针改变所指向对象的值(因为对象是常量),但是指针本身可以改变,可以指向另外的对象。例如:

int a;

const int* pl=&a;//pl是指向常量的指针

int b;

pl=&b;//正确,pl本身的值可以改变,指针本身不是常量。

*pl=1;//编译时出错,不能通p1改变所指的对象。

如果希望一个指针只能又来读取某些内存空间中的内容,但是不许覆盖不许改写。那么就可以用const来修饰这个指针所指向对象的类型。相当于一个只读的指针。

常量指针:指针类型是常量const,一旦初始化就不能改变了:

int a;

int *const p2=&a;

p2=&b;//错误,p2是常量指针,值不可修改。

9、指针的运算;算术运算(指针与整数的加减运算,指针++,指针--)

                                        

指针+1,可能是加了2个字节,也可能是加了4个字节。要由指针所指的对象类型决定。

当指针指向连续存储的同类数据时,指针与整数的加减运算和自增运算才有意义。比如:指针指向一个doube或者int类型的变量,在它加1之后指向下一个内存单元,它究竟指向什么类型就不知道了,是不确定的,没有意义。

连续存储同类数据——比如;数组。如下:指针指向数组的首地址就是元素a[0],每次加1使得每次进2个字节。

                                                            

指针的关系运算:

                                             
 

猜你喜欢

转载自blog.csdn.net/qq_21210467/article/details/82785082