C++:16.03.11 实验课总结

C++:16.03.11 实验课总结

标签: C++

by 小威威

概况:
本次实验课的代码题难度较大(对于刚刚入门C++的人来说),对于解答者有一定的要求:1.了解链表的基本操作;2.对于浅复制与深复制有一定的了解并能加以区分;3.要有较强的gdb调试能力。(没有对程序进行调试一般情况下是得不到满分的,不排除你是超级大大神,第一次就写出了没有内存泄漏的代码)

这道题涉及到的知识点:
复制构造函数中的深复制,以及指针,动态内存分配,链表的建立,插入,删除,输出,排序,和删除链表,而且还要求通过gdb调试来解决runtime error 以及 内存泄漏等问题。

题目如下:
(15 C++ Lab) D&A Simple Linked List(Author: 叶嘉祺(TA))

Introduction

Knowledge points: (copy) constructor, deep copy, pointers, dynamic allocation, linked list algorithms, debug methods(GDB or IDE or output debug), memory leak.

In this Lab, you are going to implement a class named list which is a simple version for the list in stl. You are going to use dynamic memory allocation and pointer operations to finish this project.

I recommend you to:

Learn the knowledge points mentioned above.
Use local compilers to test you program rather than submit your answer to the system time after time.
Use local debug tools(GDB is recommended) to debug your code, especially for the memory leak problem. I can tell you that you will meet runtime error problem if you don’t use local debug tools.
Make use of your paper and pen to have some sketches because it’s a good way when you meet list.

Requirements:

Finish all the functions which have been declared inside the hpp file.

Details:

string toString(void) const

Return a visible list using ‘->’ to show the linked relation which is a string like:

1->2->3->4->5->NULL

void insert(int position, const int& data)

Add an element at the given position:

example0:

1->3->4->5->NULL

instert(1, 2);

1->2->3->4->5->NULL

example1:

NULL

insert(0, 1)

1->NULL

void list::erase(int position)

Erase the element at the given position

1->2->3->4->5->NULL

erase(0)

2->3->4->5->NULL

More

Happy coding…

题目已给出main.cpp与list.hpp

// main.cpp
#include <iostream>
#include <string>
#include "list.hpp"

using std::cin;
using std::cout;
using std::endl;
using std::string;

int main() {
  list li;

  int n;
  cin >> n;

  for (int i = 0, data, pos; i < n; i++) {
    cin >> pos >> data;
    li.insert(pos, data);
  }

  cout << li.toString() << " size: " << li.size() << endl;

  list li2(li);
  list li3;

  li = li3 = li2 = li;

  cout << li.toString() << " size: " << li.size() << endl;
  cout << li2.toString() << " size: " << li2.size() << endl;
  cout << li3.toString() << " size: " << li3.size() << endl;

  int m;
  cin >> m;

  for (int i = 0, pos; i < m; i++) {
    cin >> pos;
    li.erase(pos);
  }

  cout << li.toString() << endl;

  cout << li.sort().toString() << endl;
  cout << li2.sort().toString() << endl;
  cout << li3.sort().toString() << endl;

  return 0;
}


// list.hpp
#ifndef LIST
#define LIST

#include <string>
#include <iostream>

typedef struct node {
  int data;
  struct node* next;
  node(int data = 0, struct node* next = NULL) : data(data), next(next) {}
} node;

class list {
 private:
  node* head;
  int _size;

 public:
  list();
  list(const list&);
  list& operator=(const list&);
  ~list();

  // Capacity
  bool empty(void) const;
  int size(void) const;

 public:
  // output
  // list: [1,2,3,4,5]
  // output: 1->2->3->4->5->NULL
  std::string toString(void) const;

  void insert(int position, const int& data);
  void erase(int position);
  void clear(void) {
    if (this->head != NULL) {
      node* p = this->head;
      while (p != NULL) {
        node* temp = p;
        p = p->next;
        delete temp;
      }
      this->head = NULL;
    }
    this->_size = 0;
  }
  list& sort(void);
};

#endif

现要求我们完成list.cpp来实现类list。

list.cpp需要完成类的构造函数、复制构造函数,”=”运算符的重载,析构函数,链表结点插入函数,链表结点删除函数,链表的输出函数(按照格式),检查链表是否为空的函数,返回链表结点数目的函数以及链表的排序函数的定义。

1.涉及到链表的题目,要区分一下题目建立的是何种链表。

(1)含头结点的链表(头结点不用来存储数据);

(1)不含头结点的链表(也就是第一个结点就用来存储数据)。

我是根据list.hpp中的clear函数的定义推断出是不含头结点的链表。因为在clear函数的定义中,有一个if条件是用来判断head是否为空。为什么我们要对链表的类型加以确定,原因是:对于链表的插入与删除操作,二者是不同的,若含有头结点,插入数据、删除数据都无需考虑特殊情况,因为插入后的操作都是相同的。但是对于没有头结点的链表来说,插入、删除第一个位置的操作是特殊的,需要单独拿出来定义。因此需要区分链表的类型。

2.复制构造函数与重载”=”的函数同时出现

当类的成员出现指针,一般都要进行深拷贝。若要进行深拷贝,一般都要定义自己复制构造函数。有时题目要求重载”=”,为了提高代码的重用性,我们可以在复制构造函数中使用我们重载的”=”进行复制。

3.重载”=”运算符可能引起的内存泄漏

在重载的过程中我们不仅要考虑深拷贝,更需要考虑是否会出现内存泄漏的情况。在进行复制之前,要先把等号左边的对象的链表进行内存释放。
假设我们没有在复制前进行内存释放(已考虑了深拷贝)如:

list1 = list2;

例如这个语句,我们将list2复制给list1, list1中的head指向一块新的内存,这样它就不再指向原有的那块内存,而head又是这段内存的唯一指向,这样一来就会导致head原来指向的这块内存无法再访问,这也就导致了内存泄漏。(内存泄漏可以正常通过编译,要用专门的工具才能检测出来)所以在复制前清空的操作是很重要的!!!

因此,要养成一个良好的编程习惯:在给指针赋值的时候,要考虑是否会导致原有指针指向的内存泄漏!

4.stringstream的问题

std::stringstream ss;

ss.clear()与ss.str(“”)

ss.clear()起到重置的作用,ss.str(“”)起到清空的作用。注意,clear不是清空的意思!!!

(1)当进行多次转化时(<<与>>成对使用时),在每次转化之后都要用ss.clear()进行重置;
(2)但是如果在多进行多次转化时,<<与>>不成对使用,而是用ss.str()等函数直接进行类型转化时,在每次转化后都要用ss.str(“”)进行清空。
我的代码如下:

5.链表的插入与删除

这部分知识可以看我以前写的博文:链表的入门
但是那个毕竟是我刚入门时写的,方法可能比较麻烦,但是还是很容易理解的。而我在最后展示的代码里采用了更为精简的方法来实现插入与删除操作,如果有兴趣可以看一下。

6.链表的排序

在没有时间的限制下,我们可以将链表内的数据存储到数组中,再对数组进行排序,然后再将数组内的值重新复制到链表中即可。这种方法思维量较小,可以节省不少时间。

以下便是我这道题的代码:

# include "list.hpp"
# include <string>
# include <sstream>

list :: list() {
    head = NULL;
    _size = 0;
}

list :: list(const list& list1) {
    head = NULL;
    _size = 0;
    *this = list1;
}

list& list :: operator=(const list& list1) {
    clear();   // 十分重要!!!
    _size = list1._size;
    if (list1.head == NULL) {
        head = NULL;
        _size = 0;
    } else {
        head = new node(list1.head->data);
        node *p1 = head;
        node *p2 = list1.head->next;
        int num = 1;
        while (num < list1._size) {
            p1->next = new node(p2->data); // 此处调用了node的构造函数而不是起到赋值的作用 
            p2 = p2->next;
            p1 = p1->next;
            num++;
        }
        p1->next = NULL;
    }
    return *this;
}

list :: ~list() {
    clear();
}

bool list :: empty(void) const {
    return _size ? false : true;
}

int list :: size(void) const {
    return _size;
}

std::string list :: toString(void) const {
    node *p1 = head;
    std::string result = "";
    std::string mid;
    while (p1 != NULL) {
        std::stringstream ss;
        ss << p1->data;
        ss >> mid;
        result += mid + "->";
        p1 = p1->next;
    }
    result += "NULL";
    return result;
}

void list :: insert(int position, const int& data) {
    if (position >= 0 && position <= _size) {  // 注意排除不符合情况的position
        if (position == 0) {
            node *p1 = new node(data);
            p1->next = head;
            head = p1;
            _size++;
        } else {
            node *p1 = head;
            position--;
            while (position--) {
                p1 = p1->next;
            }
            node *p2 = new node(data);
            p2->next = p1->next;
            p1->next = p2;
            _size++;
        }
    }
}

void list :: erase(int position) {
    if (position >= 0 && position < _size) {
        if (position == 0) {
            node *p1 = head->next;
            delete head;
            head = p1;
            _size--;
        } else {
            position--;
            node *p1 = head;
            while (position--) {
                p1 = p1->next;
            }
            node *p2 = p1->next;
            p1->next = p2->next;
            delete p2;
            _size--;
        }
    }
}

list& list :: sort(void) {  // 这里用了冒泡排序
    node *p1 = head;
    int *arr = new int[_size];
    int i = 0;
    while (p1 != NULL) {
        arr[i] = p1->data;
        i++;
        p1 = p1->next;
    }
    for (int j = 0; j < _size-1; j++) {
        for (int k = 0; k < _size-1-j; k++) {
            int temp;
            if (arr[k] > arr[k+1]) {
                temp = arr[k];
                arr[k] = arr[k+1];
                arr[k+1] = temp;
            }
        }
    }
    p1 = head;
    i = 0;
    while (p1 != NULL) {
        p1->data = arr[i];
        p1 = p1->next;
        i++;
    }
    delete[] arr;
    return *this;
}

以上内容皆为本人观点,欢迎大家提出批评和指导,我们一起探讨!

你可能感兴趣的:(C++,链表,gdb,内存泄漏,深复制)