C++进阶(补第三章)

3.11函数的形参—指针和引用

在前面一直说指针、引用,那么它们对函数有什么影响呢?

先看一个形参没有指针和引用的函数:

//函数1

#include<iostream>

int add(int a, int b);

int main(void)

{

using namespace std;

int m = 2, n = 3;

int i = add(m, n);

cout <<m << endl<<n<<endl<<i<<endl;

return 0;

}

int add(int a, int b)

{

a++;

b++;

return a+b;

}

运行结果:

2

3

7

形参为指针的函数:

//函数2

#include<iostream>

int add(int * a, int * b);

int main(void)

{

using namespace std;

int m = 2, n = 3;

int i = add(&m, &n); //传入m/n的地址

cout <<m << endl<<n<<endl<<i<<endl;

return 0;

}

int add(int * a, int * b)

{

(*a)++;

(*b)++;

return *a+*b;

}

运行结果:

3

4

7

形参为引用的函数:

//函数3

#include<iostream>

int add(int &a, int &b);

int main(void)

{

using namespace std;

int m = 2, n = 3;

int i = add(m, n);

cout <<m << endl<<n<<endl<<i<<endl;

return 0;

}

int add(int &a, int &b)

{

a++;

b++;

return a+b;

}

运行结果:

3

4

7

代码解释:

在函数1中,函数形参不为指针或引用,当调用add函数时,会在栈中分配内存来存放传入形参a/b的实际值,因此a/b改变,m/n不改变。

在函数2中,函数形参为指针,当调用add函数时,传入形参a/b的值实际是m/n的地址值,因此改变*a/*b,m/n也会改变。

在函数3中,函数形参为引用,引用的本质是别名,因此在调用add函数时,a/b实际上分别是m/n的别名,其操作的也是m/n的值,因此m/n改变。

综上,当我们在函数中使用指针或引用时,其“操作的”实际上是实参,而不是栈内存中的数值。

3.12结构体—数据结构

在STL容器中,有很多容器可以直接使用,如双向队列deque、双向链表list等,这些容器在C中也称为数据结构。在这一小节,我们将介绍几种简单的数据结构。

1.单向队列

#include<iostream>

using namespace std;

 

//定义队列,由节点、队首指针和队尾指针组成

struct QNode    //定义队列结点的数据结构

{

double data;  //数据域,存储队列信息

QNode *next; //指针域,指向下一个结点

};

 

struct LinkQueue    //定义队首指针和队尾指针

{

QNode *front;      //队首指针,指向头节点

QNode *rear;       //队尾指针,指向尾节点

};

 

//队列的操作函数

//初始化队列:创建节点、队首和队尾指针

void InitQueue(LinkQueue &Q)     //构造一个空的队列

{

QNode *q = nullptr;

q = new QNode;    //申请一个结点的空间

q->next = nullptr;   //q为头结点,由于只有一个节点,因此q也是尾节点,因此next为null   

Q.front = q;   //队首与队尾指针都指向这个结点

Q.rear = q;

}

//判断队列是否为空

bool IsEmpty(LinkQueue &Q)    

{

if (Q.front == Q.rear)  //当队列的头节点和尾节点指向同一个队列节点,为空(只有一个节点)

return 0;

else

return 1;

}

//进队,从队列尾部插入元素

QNode * InQueue(LinkQueue &Q, double data)     

{

QNode *p = nullptr; //新创建一个结点

p = new QNode;

p->data = data;  //输入数据信息

p->next = nullptr;

Q.rear->next = p; //将新结点插入队列尾部,因此前面一个节点的next指向p

Q.rear = p;       //将新结点插入队列尾部后,前面节点不再是尾节点,将p设置为新的尾结点

//return *p; //堆空间不能返回实体(QNode),否则在调用函数的栈空间存放此实体(QNode)

return p;

}

 

//出队,从队列首部删除一个结点

bool OutQueue(LinkQueue &Q)   

{

QNode *p = nullptr;

if(Q.front->next) //如果只有一个节点,Q.front->next == null

{

p = Q.front; //将原来的头结点存放在p中

Q.front = Q.front->next; //第二个节点设置为头结点

delete p; //释放堆空间,不需要指定p为野指针,因为函数结束,p销毁

return 1;

}

else

return 0;

}

//打印函数

void Print_QNode(QNode & q)

{

cout << "The address of this node is " << &q << endl;

cout << "The data of this node is "<<q.data << endl;

cout << "The address of next node is " << q.next << endl;

}

 

int main(void)

{

LinkQueue Queue;

InitQueue(Queue);

if (!IsEmpty(Queue))

cout << "this is a enmpty queue!" << endl;

QNode * qnode1;

qnode1 = InQueue(Queue, 2.0);

Print_QNode(*qnode1);

QNode * qnode2;

qnode2 = InQueue(Queue, 2.1);

Print_QNode(*qnode1);

Print_QNode(*qnode2);

OutQueue(Queue); //删除第一个创建的空节点

OutQueue(Queue);//删除节点qnode1

Print_QNode(*qnode1); //已被释放,因此数据全是乱的

Print_QNode(*qnode2);

return 0;

}

运行结果:

this is a enmpty queue!

The address of this node is 00D07930

The data of this node is 2

The address of next node is 00000000

The address of this node is 00D07930

The data of this node is 2

The address of next node is 00D07970

The address of this node is 00D07970

The data of this node is 2.1

The address of next node is 00000000

The address of this node is 00D07930

The data of this node is -1.45682e+144

The address of next node is DDDDDDDD

The address of this node is 00D07970

The data of this node is 2.1

The address of next node is 00000000

单向队列由连续的节点组成,包括队首指针和队尾指针。单向队列只允许从队尾处插入节点,从队首处删除节点。队列的创建、插入、删除等操作在前面的代码中注释的很明白了。

2.单向链表

#include<iostream>

using namespace std;

 

//定义链表

struct LNode    //定义链表结点的数据结构

{

double data;  //数据域,存储队列信息

LNode *next; //指针域,指向下一个结点

};

 

struct LinkList    //定义链表的辅助数据

{

LNode *head;    //头节点

int length; //表示链表的长度,即当前链表的节点数

int size; //表示链表的大小,即可以容纳的节点数

};

 

//链表的操作函数

//初始化链表

void InitList(LinkList &L,int s)     //构造链表的头结点

{

LNode * list = nullptr;

list = new LNode;

list->next = nullptr;

L.head = list;

L.length = 1;  //链表节点从1开始计数

L.size = s;

}

//判断链表是否为满

bool IsFull(LinkList &L)

{

if (L.length == L.size)  

return 1;

else

return 0;

}

//插入链表

bool InList(LinkList &L,int addr, double data) //addr为插入节点的位置,即在节点addr后面插入新的节点

{

if (addr > L.length || addr >= L.size)

return 0;

LNode * list = nullptr;

list = L.head;

for (int i = 1; i < addr; i++)  //找到对应节点的地址

{

list = list->next;

}

LNode * list_new = nullptr;

list_new = new LNode;

if (addr == L.length)  //对尾节点做处理

{

list_new->data = data;

list_new->next = nullptr;

list->next = list_new;

}

else

{

list_new->data = data;

list_new->next = list->next;

list->next = list_new;

}

L.length++;

return 1;

}

 

//从链表中删除某个节点

bool OutList(LinkList &L, int addr) //addr为链表中要删除节点的位置

{

if (addr > L.length || addr > L.size)

return 0;

LNode * list = nullptr;

list = L.head;

for (int i = 1; i < addr-1; i++)  //找到对应节点的上一个节点地址

{

list = list->next;

}

if (addr == L.length)  //对尾节点做处理

{

LNode * list_cur = list->next;

list->next = nullptr;

delete list_cur;

}

else

{

LNode * list_cur = list->next;

list->next = list->next->next;

delete list_cur;

}

L.length--;

return 1;

}

//打印对应节点

void Print_LNode(LinkList &L, int addr)

{

if (addr > L.length)

cout << "This list does not have this node" << endl;

LNode * list = nullptr;

list = L.head;

for (int i = 1; i < addr; i++)  //找到对应节点

{

list = list->next;

}

cout << "The address of this node is " << list << endl;

cout << "The data of this node is "<<list->data << endl;

cout << "The address of next node is " << list->next << endl;

}

 

int main(void)

{

LinkList List;

InitList(List,4);  //初始化后,length为1,表示头结点,size为4

InList(List, 1, 1.2); //插入第一个节点的后面

Print_LNode(List, 2); //打印第二个节点

InList(List, 2, 2.2); //插入第二个节点的后面

Print_LNode(List, 2); //打印第二个节点

Print_LNode(List, 3); //打印第三个节点

InList(List, 2, 5.2); //插入第二个节点的后面

Print_LNode(List, 2); //打印第二个节点

Print_LNode(List, 3); //打印第三个节点

Print_LNode(List, 4); //打印第四个节点

if (IsFull(List))

cout << "this list is full" << endl;

OutList(List, 3);

Print_LNode(List, 2); //打印第二个节点

Print_LNode(List, 3); //打印第三个节点

return 0;

}

运行结果:

The address of this node is 012479B0

The data of this node is 1.2

The address of next node is 00000000

The address of this node is 012479B0

The data of this node is 1.2

The address of next node is 01247838

The address of this node is 01247838

The data of this node is 2.2

The address of next node is 00000000

The address of this node is 012479B0

The data of this node is 1.2

The address of next node is 01247878

The address of this node is 01247878

The data of this node is 5.2

The address of next node is 01247838

The address of this node is 01247838

The data of this node is 2.2

The address of next node is 00000000

this list is full

The address of this node is 012479B0

The data of this node is 1.2

The address of next node is 01247838

The address of this node is 01247838

The data of this node is 2.2

The address of next node is 00000000

链表和队列都是非连续的存储结构,但是链表可以在任何位置进行删除、插入,而单向队列只能在头节点删除、尾节点插入。

3.循环队列

#include<iostream>

using namespace std;

 

//定义循环队列

struct CircleQueue    //定义队列结点的数据结构

{

double * data;  //数据

int front; //队首位置

int rear;  //队尾位置,指向的空间永远是空的,即不存放数据

int size;  //循环队列的大小,可以存放size-1个数据,因为队列为空时:front == rear;队列满时:front == (rear+1)%size。

};

 

//循环队列的操作函数

//初始化队列

void Init_CircleQueue(CircleQueue &CQ,int maxsize)

{

CQ.data = new double[maxsize]; // 或CQ.data =(double *) malloc(sizeof(double) * maxsize);

if (!CQ.data)

{

cout << "创建失败..." << endl;

exit(1);

}

CQ.front = 0;

CQ.rear = 0;

CQ.size = maxsize;

}

 

//判断循环队列是否为满

bool IsFull(CircleQueue &CQ)

{

//rear指向的空间不存放数据,因此可存放size-1个数据。当循环队列满时,front和rear相邻。

//因此CQ.rear+1使front和rear重合,%CQ.size是使front和rear一直在0~size之间循环。

if( CQ.front == (CQ.rear+1)%CQ.size )

return 1;

else

return 0;

}

//进队,从循环队列尾部插入元素

 bool In_CircleQueue(CircleQueue &CQ, double data)

{

if (IsFull(CQ))

return 0;

CQ.data[CQ.rear] = data;

CQ.rear = (CQ.rear + 1) % CQ.size; //这样rear一直在0~size之间循环

return 1;

}

 

//出队,从循环队列首部删除一个结点

bool Out_CircleQueue(CircleQueue &CQ)

{

if (CQ.front == CQ.rear)

return 0;

CQ.front = (CQ.front + 1) % CQ.size;

return 1;

}

//打印函数

void Print_CircleQueue(CircleQueue & CQ)

{

int i = CQ.front;

for (; i != CQ.rear; i = (i + 1) % CQ.size)

{

printf("CQ.data[%d] = ", i);

cout << CQ.data[i] << endl;

}

}

 

int main(void)

{

CircleQueue cq;

Init_CircleQueue(cq, 5);

In_CircleQueue(cq, 1.2);

In_CircleQueue(cq, 2.2);

In_CircleQueue(cq, 3.2);

In_CircleQueue(cq, 4.2);

Print_CircleQueue(cq);

if (!In_CircleQueue(cq, 5.2))

cout << "入队失败,循环队列已满" << endl;

if (Out_CircleQueue(cq))

cout << "出队成功" << endl;

Print_CircleQueue(cq);

In_CircleQueue(cq, 5.2);

Print_CircleQueue(cq);

return 0;

}

运行结果:

CQ.data[0] = 1.2

CQ.data[1] = 2.2

CQ.data[2] = 3.2

CQ.data[3] = 4.2

入队失败,循环队列已满

出队成功

CQ.data[1] = 2.2

CQ.data[2] = 3.2

CQ.data[3] = 4.2

CQ.data[1] = 2.2

CQ.data[2] = 3.2

CQ.data[3] = 4.2

CQ.data[4] = 5.2

使用循环队列比前面的单向队列简单多了,不必每一次入队都需要申请动态空间,也不需要考虑出队后堆空间的释放问题。这里有一个问题需要注意:当front和rear重合时,判定为队列空;当front == (rear + 1) % size判定为队列满;rear指向的位置是永远不存放数据的,因此判断满时需要将rear加1,并且front和rear不能超出size的范围,因此对siez取余。

总结:在此仅介绍了3种简单的数据结构,这些都是在C中的实现。有兴趣的同学可以自己动手,在C++中将其放入类中来实现。

3.13 关键字

1.define

用法1:

#define SIZE 100

define定义的标识符SIZE可以在代码中代替整数100,其作用域是从它定义开始的地方一直到文件结尾(不能跨文件,除非包含此头文件)。

使用define有几个需要注意的地方:开头带#号,结尾没有逗号;为了防止文件中会定义重名的标识符,因此define定义的标识符一般都是大写,并且有“_”相连接,如MAX_SIZE;一般都是将define定义的标识符放到一个头文件中,这样用到时只需要包含一个头文件即可。

用法2:

#define Add(a,b) a+b

使用define来函数定义,虽然方便,但是没有类型检查,不太安全。另外还有一个问题,执行2*Add(1, 2)的结果是4,因为它执行的是2*1+2 = 4。

解决方法:

#define Add(a,b) (a+b)

cout << 2*Add(1, 2) << endl;  //打印6

用法3:

#ifndef  __MYFILE_H__

#define  __MYFILE_H__

...

文件内容

...

#endif

这是防止头文件被重复包含。

用法4:

#define VXWORKS

#ifdef VXWORKS

cout << "vxworks" << endl;

#else

cout << "windows" << endl;

#endif

常用于跨平台开发的代码中。

2.typedef

typedef和define很相似,但typedef功能更具有局限性,即typedef用于定义类型别名(跟引用也很相似)。

typedef CircleQueue queue;  //当定义结构体名字太长时,可以用typedef

typedef int * pri;

pri m,n;   //m、n数据类型为int型指针

define是使用前面代替后面,而typedef是使用后面代替前面。

3.auto

auto称为自动类型推断。

vector<int> vi = {1,2,3,4};

auto vii = vi.begin();

还记得前面的类模板vector吗?其成员函数begin会返回起始元素的迭代器,因此可以使用auto来代替下面的2行代码。

vector<int> vi = {1,2,3,4};

vector<int>::iterator vii;

vii = vi.begin();

4.volatile

在以前刚开始调单板时,可能经常会遇到这样的程序:

#define GPFCON (*(volatile unsigned long *)0x56000050)

GPFCON是一个寄存器变量,它的地址是0x56000050,数据类型是volatile unsigned long。那么这个volatile到底是什么意思呢?

当一个变量加volatile表示编译器对此变量不再优化,每次从它的地址中进行读取。以防遇到某些编译器无法探知的因素时,如操作系统、其他线程或中断等操作对变量进行修改。编译器优化后读取变量是不会从地址开始读的,如变量flag若在程序中没有修改,则编译器会把它优化,存放在某个寄存器中以供读取。

常用在:中断服务程序中修改了供其他程序使用的变量;多任务环境下共享的变量;存储器映射的硬件寄存器。

5.static

在一个源码文件中,函数之外定义的变量称为全局变量,函数内部定义的变量称为局部变量。当全局变量前加上static,就变为了静态全局变量;局部变量前加上static,就变成了静态局部变量。

全局变量在其定义的源文件中是有效的,即这个源文件中的所有函数都可以使用这个全局变量。但全局变量有个缺点,对于项目下的所有源文件,此全局变量都是可见的,也就是说在项目下的其他源文件中不可以重新定义一个同名的全局变量。若要在其他源文件中使用这个全局变量,则需要加extern关键字。

若一个项目的源文件很多,当需要定义一个全局变量时,岂不是要查看其他所有的源文件,看看这个全局变量有没有定义过?解决方法就是加上static,定义静态全局变量。静态全局变量的有效性和可见性都只限于定义的源文件中。

//main.cpp

static int error; //int error则会出错

void fun_test();

int main(void)

{

fun_test();

fun_test();

fun_test();

}

//file1.cpp

#include<iostream>

using namespace std;

int error;

void fun_test()

{

static int i;  //静态局部变量只初始化一次

cout << i << endl;

i++;

}

运行结果:

0

1

2

在一个项目中,有2个源文件,分别是main.cpp和file1.cpp。2个源文件中都定义了error,若都不加static则编译不通过,因此需要一个源文件中定义静态全局变量。

总结:在自己进行项目开发时,代码中少使用全局变量,尽量使用静态全局变量;若静态全局变量不初始化,则编译器会自动初始化为0。

局部变量定义在函数体内,当函数执行结束后,此变量也会销毁。若我们需要一个变量,在函数调用结束后依旧保存,而不随函数结束而销毁。这样的一个变量就是静态局部变量。在前面的file.cpp文件的fn_test()函数中,定义了静态局部变量i,它没有随fn_test()执行完销毁,而是保存了自己的数值,等待fn_test()下次调用时使用。为什么会这样呢?因此局部变量保护在栈中,而静态局部变量保存在全局数据存储区。

总结:静态局部变量不随函数执行完而销毁,而是等待函数下次调用时再继续使用;静态局部变量只初始化一次,若不初始化则编译器会初始化为0,其存放在全局数据存储区。

函数和全局变量类似,函数对于所有的源文件都是有效的;若想定义的函数不能被其他源文件使用,则需要定义静态函数。

注:当程序中调用某个函数时,编译器该怎么去找它呢?若是静态函数,则只在定义的源文件中查找;若不是,则在所有的源文件中查找,若找到2个则编译出错,若一个都找不到则去库中查找。

//main.cpp

#include<iostream>

using namespace std;

static int error; //int error则会出错

void fun_test();

int main(void)

{

fun_test();

fun_test();

fun_test();

}

void fun_test()

{

static int i;  //静态局部变量只初始化一次

cout << i << endl;

i++;

}

//file.cpp

#include<iostream>

using namespace std;

int error;

 

static void fun_test()

{

static int i;  //静态局部变量只初始化一次

cout << i << endl;

i++;

}

总结:静态函数不能被其他源文件使用;其他源文件中可以定义相同名字的函数,而不会发生冲突。

6.extern

//main.cpp

#include<iostream>

using namespace std;

extern int error; //int error则会出错

void fun_test();

int main(void)

{

fun_test();

fun_test();

fun_test();

cout << error << endl;

}

//file.cpp

#include<iostream>

using namespace std;

int error = 10;

 

void fun_test()

{

static int i;  //静态局部变量只初始化一次

cout << i << endl;

i++;

}

当全局变量定义在file.cpp中,而main.cpp中需要使用此全局变量时,在函数外用extern声明此全局变量即可。注意:只有在需要使用此全局变量的源文件中才可以使用extern声明,并且此全局变量不能再初始化(C++中变量只能初始化一次)。

7.const

当我们不能改变变量的数值时,称为该变量只读(又称常量),在C++中使用const关键字来实现。

const int i = 10;  //正确

const int j; //错误

const只能在变量初始化时才可以使用,变量定义和赋值都不可以使用。

//main.cpp

#include<iostream>

using namespace std;

extern const int error;

void fun_test();

int main(void)

{

fun_test();

fun_test();

fun_test();

cout << error << endl;

}

//file.cpp

#include<iostream>

using namespace std;

extern const int error = 10; // const int error = 10会出错

 

void fun_test()

{

static int i;  

cout << i << endl;

i++;

}

const在函数外初始化的变量跟全局变量一样,都是在定义的源文件中有效。但const定义的变量要在其他源文件中使用时,需要在其初始化时就加extern,这点和全局变量不同。

const和引用:

int m = 10;

const int n = 12;

(1)

const int &mrf = m; //正确,int可以转化为const int引用

//int &nrf = n;  //错误,const int不能转化为int引用

const int &nrf = n;  //正确,const int转化为const int引用

(2)

m = 20;

cout<<mrf<<endl; //打印20,通过修改m来修改mrf

//n = 15;  //错误,n只读不能写

//nrf = 11; //错误,nrf 只读不能写

(3)

//int &z = nrf*2; //错误,const int不能转化为int引用

const int &z = nrf*2; //正确,const int转化为const int引用

//int z = nrf*2;  //正确,赋值无关变量的读写

引用和const一样都是在变量初始化时才可以使用,const引用又称常量引用。

const和指针:

double pb = 10;

const double pl = 12;

(1)

const double * ptr1 = &pb;   //正确,double *可以转化为const double *

//double * ptr2 = &pl;  //错误,const double *不可以转化为double *

const double * ptr2 = &pl;  //正确,const double *可以转化为const double *

(2)

* ptr1 = 15; //错误,* ptr1不能修改

ptr1 = &pl;  //正确,ptr1可以修改

pb = 15;   //正确

cout << *ptr2 << endl; //打印15

(3)

double const * ptr3 = &pb;

// *ptr3 = 20;  //错误,*ptr3不可修改

ptr3 = &pl;  //正确,ptr3可修改

(4)

double *  const ptr4 = &pb;

//ptr4 = &pl;  //错误,ptr4不可修改

*ptr4 = 20;  //正确, *ptr4可修改

在double *中,根据const位置的不同,可修改的变量也不同。const double *和double const *是一样的,即指针可修改,指针指向的变量不可修改。当const放在double *的后面时,指针不可修改,指针指向的变量的值可以修改,但是前提是指针指向的变量是可写的。

const和char *字符串:

// char * cp = "hello world";  //以前是可以这样写的,但现在不行了

const char * cp = "hello world"; //或char const * cp = "hello"这2种方式是一样的

cp = "new hello world";  //cp可以修改

//cp[0] = 'c';  //错误,*cp不可修改

const char *表示指针可以改,指针指向的变量不可以修改

3.14内存模型和new运算符

1.C++存储数据的四种方式

全局存储区:存放全局变量、静态全局变量和静态局部变量。全局变量和静态全局变量在程序(通常指main函数)启动时分配,程序结束时销毁;静态局部变量在函数中第一次分配是创建,程序结束时销毁。

栈存储区:存放局部变量,在函数执行时分配,在函数结束时销毁。

堆存储区:由new运算符分配,由delete运算符释放或直到程序结束时释放。

线程存储区(C++11):使用thread_local声明,其生命周期和所属线程相同。

2.new/delete运算符

new:

int * pi1 = new int;

int * pi2 = new int(2);

int * pi3 = new int[3];

int * pi4 = new int[3]{ 1,2,3 };

delete:

delete pi1;

delete pi2;

delete []pi3;

delete []pi4;

定位new运算符:

char  buffer[512];

int * pc1 = new (buffer) int[10];

double * pc2 = new (buffer) double[10];

常规的new运算符分配的堆空间需要使用delete来释放;但是使用定位new运算符分配的空间不需要使用delete释放,因为它存放在全局存储区中。当new分配失败时,并不返回空指针,而是引发std::bad_alloc异常。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值