Вопрос: Как использовать «гибкость» двумерного массива, перед одномерным?

Лучший ответ:
Итак, перед нами задача: сделать динамический массив «умных указателей единоличного владения». Умный указатель единоличного владения (std::unique_ptr из C++11) — это указатель, который самолично владеет выделенной памятью; при исчезновении указателя исчезает и память.
Раз мы только учимся, мы не будем влезать в самописные шаблоны C++, готовые шаблоны STL (кроме algorithm и string) и новый, но уже реализованный во всех компиляторах стандарт C++11. Это довольно серьёзное ограничение; если его снять, можно серьёзно упростить себе жизнь. А для этого мы отделим структуру данных от жизненных объектов и реализуем объект StudentList. Пишу с листа, возможны ошибки.
Да, и без C++11 умный указатель единоличного владения реализовать довольно тяжело — поэтому структуру данных будем делать «монолитно», без разделения на умный указатель и динамический массив.#include <algorithm>

class StudentList
{
public:
  StudentList();
  ~StudentList();
  Student& add();   // добавить пустого студента и выдать ссылку на новенького
  size_t size() const { return fSize; }
  Student& operator[](size_t i) { return *fData[i]; }   // можно также наладить проверку диапазона — сделай это сам…
  const Student& operator[](size_t i) const { return *fData[i]; }
  void clear();
private:
  typedef Student* PStudent;
  PStudent* fData;
  size_t fSize, fCapacity;   // реальное кол-во студентов и на сколько студентов у нас заведено памяти.
                        // Указатели [fSize..fCapacity) резервные, их значение не определено и высвобождать
                        // их не надо.
  enum { BLOCK_SIZE = 16; };
  StudentList(const StudentList&) {}   // копирование запрещаем, хочешь — реализуй сам и вынеси в public
  StudentList& operator=(const StudentList&) { return *this; }  // аналогично
};

StudentList::StudentList(); : fData(NULL), fSize(0), fCapacity(0) {}

Student& StudentList::add()
{
  // Убедиться, что массива хватает; если нет — расширить
  if (fSize >= fCapacity) {
    size_t newCapacity = fCapacity + BLOCK_SIZE;
    PStudent* newData = new PStudent[newCapacity];
    std::copy(fData, fData + fSize, newData);
    delete[] fData;
    fData = newData;
    fCapacity = newCapacity;
  }
  // Завести нового студента
  Student* r = new Student;
  fData[fSize++] = r;
  return *r;
}

void StudentList::clear()
{
   for (size_t i = 0; i < fSize; ++i)
     delete fData[i];
   delete[] fData;
   fData = NULL;
   fSize = 0;
   fCapacity = 0;
}

StudentList::~StudentList()
{
   clear();
}
Удаление и прочее сам наладишь?
А группа будет пользоваться нашим списком.#include <string>

class AcademyGroup
{
public:
   std::string name;
   StudentList students;  // при желании можно заинкапсулировать и его.
};
Это перед нами, правда, не двухмерный массив, как я уже сказал. Массив, хоть и Student**, но одномерный; каждый элемент массива — умный указатель единоличного владения. Если бы мы писали на STL C++11, это был бы std::vector<std::unique_ptr<Student>>.
Кроме УУЕВ, существует также умный указатель совместного владения std::shared_ptr.
Можно сделать второй вариант — массив из одномерных массивов. Если он заполнился — заводим ещё один массивчик. Пишется немного сложнее, особенно если не пользоваться STL. На STL — std::deque<Student>.

Худший ответ:
В вашей структуре данных не двумерный массив используется, а массив указателей.
Массив указателей вам всё равно придётся двигать при добавлении\удалении элементов. Но это эффективнее, чем двигать массив объектов.