Подтвердить что ты не робот

Конструкции С++, заменяющие конструкции C

После обсуждения с недавно появившимся разработчиком в моей команде я понял, что в С++ существуют привычки использовать конструкторы C, потому что они должны быть лучше (т.е. быстрее, более компактно, красивее, выберите причину).

Какими примерами стоит поделиться, показывая конструкции C, по сравнению с аналогичной конструкцией С++?

Для каждого примера мне нужно прочитать причины, по которым конструкция С++ так же хороша или даже лучше оригинальная конструкция C. Цель состоит в том, чтобы предложить альтернативы некоторым C-конструкциям, которые считаются несколько опасными/небезопасными в С++-коде (С++ 0x действительны только ответы принимаются до тех пор, пока они четко обозначены как С++ 0x).

В качестве примера я опубликую ниже ответ (встроенная инициализация структуры).

Примечание 1: Пожалуйста, один ответ за случай. Если у вас несколько случаев, отправьте несколько ответов

Примечание 2: Это не вопрос C. Не добавляйте тег "C" к этому вопросу. Это не должно стать борьбой между С++ и C. Только изучение некоторых конструкций подмножества C С++ и их альтернативы в других инструментариях С++ "

Примечание 3: Это не вопрос C-bashing. Мне нужны причины. Потрясающие, избивающие и недоказанные сравнения будут сбиты с толку. Упоминание функций С++ без эквивалента C можно рассматривать как не относящееся к теме: я хочу, чтобы рядом были добавлены функции C в отношении функции С++.

4b9b3361

Ответ 1

RAII и вся последующая слава против сбора/выпуска ресурсов вручную

В C:

Resource r;
r = Acquire(...);

... Code that uses r ...

Release(r);

где в качестве примеров Resource может быть указателем на память, а Acquire/Release будет выделять/освобождать эту память, или это может быть дескриптор открытого файла, где Acquire/Release откроет/закрывает этот файл.

Это создает ряд проблем:

  • Вы можете забыть позвонить Release
  • Информация о потоке данных для r не передается кодом. Если r будет получен и выпущен в пределах одного и того же объема, код не самодокументирует это.
  • В течение времени между Resource r и r.Acquire(...), r фактически доступен, несмотря на то, что он не инициализирован. Это источник ошибок.

Применяя методологию RAII (Инициализация ресурсов), в С++ мы получаем

class ResourceRAII
{
  Resource rawResource;

  public:
  ResourceRAII(...) {rawResource = Acquire(...);}
  ~ResourceRAII() {Release(rawResource);}

  // Functions for manipulating the resource
};

...

{
  ResourceRAII r(...);

  ... Code that uses r ...
}

Версия С++ гарантирует, что вы не забудете освободить ресурс (если это так, у вас есть утечка памяти, что легче обнаружить с помощью инструментов отладки). Это заставляет программиста быть явным о том, как поток данных ресурса (то есть: если он существует только во время области функции, это будет понятно из-за конструкции ResourceRAII в стеке). Между созданием объекта ресурса и его уничтожением не существует смысла, когда ресурс недействителен.

Это тоже безопасное исключение!

Ответ 2

Макросы против встроенных шаблонов

Стиль C:

#define max(x,y) (x) > (y) ? (x) : (y)

Стиль С++

inline template<typename T>
const T& max(const T& x, const T& y)
{
   return x > y ? x : y;
}

Причина выбора подхода С++:

  • Тип безопасности. Обеспечивает, чтобы аргументы были одного типа.
  • Синтаксические ошибки в определении max указывают на правильное место, а не на то, где вы вызываете макрос
  • Может отлаживаться в функции

Ответ 3

Динамические массивы против контейнеров STL

C-стиль:

int **foo = new int*[n];
for (int x = 0; x < n; ++x) foo[x] = new int[m];
// (...)
for (int x = 0; x < n; ++x) delete[] foo[x];
delete[] foo;

С++ - стиль:

std::vector< std::vector<int> > foo(n, std::vector<int>(m));
// (...)

Почему контейнеры STL лучше:

  • Они изменяются по размеру, массивы имеют фиксированный размер.
  • Они безопасны для исключений - если в (...) возникает необработанное исключение, тогда память массива может протекать - контейнер создается в стеке, поэтому он будет уничтожен должным образом во время размотки
  • Они выполняют проверку привязки, например. vector:: at() (выход из границ в массиве скорее всего приведет к нарушению доступа и завершению работы программы)
  • Они проще в использовании, например. vector:: clear() против ручной очистки массива
  • Они скрывают детали управления памятью, делая код более удобочитаемым

Ответ 4

#define vs. const

Я продолжаю видеть такой код у разработчиков, которые долгое время кодировали C:

#define MYBUFSIZE 256

.  .  . 

char somestring[MYBUFSIZE];

и т.д.. и др.

В С++ это будет лучше:

const int MYBUFSIZE = 256;

char somestring[MYBUFSIZE];

Конечно, еще лучше было бы разработчику использовать std::string вместо массива char, но это отдельная проблема.

Проблемы с макросами C - это легион, при этом проверка типа не является основной проблемой.

Из того, что я видел, это, по-видимому, чрезвычайно сложная привычка для программистов C, которые переходят на С++, чтобы сломаться.

Ответ 5

Параметры по умолчанию:

C:

void AddUser(LPCSTR lpcstrName, int iAge, const char *lpcstrAddress);
void AddUserByNameOnly(LPCSTR lpcstrName)
  {
  AddUser(lpcstrName, -1,NULL);
  }

C++ замена/эквивалент:

void User::Add(LPCSTR lpcstrName, int iAge=-1, const char *lpcstrAddress=NULL);

Почему это улучшение:

Позволяет программисту записывать функцию программы в меньшем количестве строк исходного кода и в более компактной форме. Также позволяет использовать значения по умолчанию для неиспользуемых параметров, наиболее близкие к тому, где они фактически используются. Для вызывающего, упрощает интерфейс к классу/структуре.

Ответ 6

C qsort function versus С++ 'sort шаблон функции. Последний предлагает безопасность типов через шаблоны, которые имеют очевидные и менее очевидные последствия:

  • Безопасность типов делает код менее подверженным ошибкам.
  • Интерфейс sort немного проще (нет необходимости указывать размер элементов).
  • Компилятор знает тип функции сравнения. Если вместо указателя функции пользователь передает объект функции, sort будет работать быстрее, чем qsort, потому что вложение сравнения становится тривиальным. Это не относится к указателям функций, которые необходимы в версии C.

В следующем примере показано использование qsort versus sort в массиве C-стиля int.

int pint_less_than(void const* pa, void const* pb) {
    return *static_cast<int const*>(pa) - *static_cast<int const*>(pb);
}

struct greater_than {
    bool operator ()(int a, int b) {
        return a > b;
    }
};

template <std::size_t Size>
void print(int (&arr)[Size]) {
    std::copy(arr, arr + Size, std::ostream_iterator<int>(std::cout, " "));
    std::cout << std::endl;
}

int main() {
    std::size_t const size = 5;
    int values[] = { 4, 3, 6, 8, 2 };

    { // qsort
        int arr[size];
        std::copy(values, values + size, arr);
        std::qsort(arr, size, sizeof(int), &pint_less_than);
        print(arr);
    }

    { // sort
        int arr[size];
        std::copy(values, values + size, arr);
        std::sort(arr, arr + size);
        print(arr);
    }

    { // sort with custom comparer
        int arr[size];
        std::copy(values, values + size, arr);
        std::sort(arr, arr + size, greater_than());
        print(arr);
    }
}

Ответ 7

встроенная инициализация структуры и встроенные конструкторы

Иногда нам нужно на С++ простое агрегирование данных. Данные, несколько независимые, защищающие его посредством инкапсуляции, не будут стоить усилий.

// C-like code in C++
struct CRect
{
   int x ;
   int y ;
} ;

void doSomething()
{
   CRect r0 ;               // uninitialized
   CRect r1 = { 25, 40 } ;  // vulnerable to some silent struct reordering,
                            // or adding a parameter
}

; Я вижу три проблемы с приведенным выше кодом:

  • Если объект не инициализирован специально, он не будет инициализирован всеми
  • Если мы заменим x или y (по какой-либо причине), инициализация по умолчанию C в doSomething() теперь будет неправильной
  • если мы добавим элемент z, и по умолчанию ему понравилось "ноль", нам все равно нужно будет изменить каждую встроенную инициализацию

В приведенном ниже коде будут встроены конструкторы (если это действительно полезно) и, следовательно, будут иметь нулевую стоимость (как код C выше):

// C++
struct CRect
{
   CRect() : x(0), y(0) {} ;
   CRect(int X, int Y) : x(X), y(Y) {} ;
   int x ;
   int y ;
} ;

void doSomething()
{
   CRect r0 ;
   CRect r1(25, 40) ;
}

(Бонус в том, что мы могли бы добавить методы operator ==, но этот бонус не соответствует теме, и поэтому стоит упомянуть, но не стоит как ответ.)

Изменить: C99 имеет инициализированный

Адам Розенфилд сделал интересный комментарий, который мне очень интересен:

C99 допускает именованные инициализаторы:   CRect r = {.x = 25,.y = 40}

Это не будет компилироваться в С++. Я думаю, это должно быть добавлено в С++, если только для C-compatibiliy. Во всяком случае, в C, он устраняет проблему, упомянутую в этом ответе.

Ответ 8

iostream vs stdio.h

В C:

#include <stdio.h>

int main()
{
    int num = 42;

    printf("%s%d%c", "Hello World\n", num, '\n');

    return 0;
}

Строка формата анализируется во время выполнения, что означает, что она не безопасна для типа.

в С++:

#include <iostream>

int main()
{
    int num = 42;

    std::cout << "Hello World\n" << num << '\n';
}

Типы данных известны во время компиляции, а также меньше, потому что нет необходимости в строке формата.

Ответ 9

Следуя сообщению fizzer в конструкциях С++, заменяющих конструкции C, я напишу здесь свой ответ:

Предупреждение: предлагаемое ниже решение С++ не является стандартным С++, но является расширением для g++ и Visual С++ и предлагается как стандарт для С++ 0x (благодаря Fizzer об этом)

Обратите внимание, что отчет Йоханнеса Шауба - litb предлагает другой, совместимый с С++ 03 способ сделать это в любом случае.

Вопрос

Как извлечь размер массива C?

Предлагаемое решение C

Источник: Когда макросы С++ полезны?


#define ARRAY_SIZE(arr) (sizeof arr / sizeof arr[0])

В отличие от "предпочтительного" решения шаблона, обсуждаемого в текущем потоке, вы можете использовать его как постоянное выражение:

char src[23];
int dest[ARRAY_SIZE(src)];

Я не согласен с Fizzer, поскольку существует шаблонное решение, способное генерировать постоянное выражение (на самом деле очень интересной частью шаблонов является их способность генерировать постоянные выражения при компиляции)

В любом случае, ARRAY_SIZE - это макрос, способный извлекать размер массива C. Я не буду подробно останавливаться на макросах в С++. Цель состоит в том, чтобы найти равное или лучшее решение на С++.

Лучшее решение на С++?

Следующая версия С++ не имеет никаких проблем с макросом и может делать все так же:

template <typename T, size_t size>
inline size_t array_size(T (&p)[size])
{
   // return sizeof(p)/sizeof(p[0]) ;
   return size ; // corrected after Konrad Rudolph comment.
}

демонстрация

Как показано в следующем коде:

#include <iostream>

// C-like macro
#define ARRAY_SIZE(arr) (sizeof arr / sizeof arr[0])

// C++ replacement
template <typename T, size_t size>
inline size_t array_size(T (&p)[size])
{
   // return sizeof(p)/sizeof(p[0]) ;
   return size ; // corrected after Konrad Rudolph comment.
}

int main(int argc, char **argv)
{
   char src[23];
   char * src2 = new char[23] ;
   int dest[ARRAY_SIZE(src)];
   int dest2[array_size(src)];

   std::cout << "ARRAY_SIZE(src)  : " << ARRAY_SIZE(src) << std::endl ;
   std::cout << "array_size(src)  : " << array_size(src) << std::endl ;
   std::cout << "ARRAY_SIZE(src2) : " << ARRAY_SIZE(src2) << std::endl ;
   // The next line won't compile
   //std::cout << "array_size(src2) : " << array_size(src2) << std::endl ;

   return 0;
}

Это выведет:

ARRAY_SIZE(src)  : 23
array_size(src)  : 23
ARRAY_SIZE(src2) : 4

В приведенном выше коде макрос ошибочно принимает указатель на массив и, таким образом, возвращает неправильное значение (4, а не 23). Вместо этого шаблон отказался компилировать:

/main.cpp|539|error: no matching function for call to ‘array_size(char*&)’|

Таким образом, демонстрируя, что решение шаблона: * возможность генерировать постоянное выражение во время компиляции * возможность остановить компиляцию, если используется неверно.

Заключение

Таким образом, в целом аргументы для шаблона:

  • отсутствие макроподобного загрязнения кода
  • может быть скрыто внутри пространства имен
  • может защитить от неправильной оценки типа (указатель на память не является массивом)

Примечание: Спасибо за реализацию Microsoft strcpy_s для С++... Я знал, что это послужит мне в один прекрасный день... ^ _ ^

http://msdn.microsoft.com/en-us/library/td1esda9.aspx

Изменить: решение является расширением, стандартизованным для С++ 0x

Fizzer справедливо прокомментировал, что это недействительно в текущем стандарте С++, и было совершенно верно (как я мог проверить на g++ с проверенной опцией -pedantic).

Тем не менее, не только это можно использовать сегодня на двух основных компиляторах (например, Visual С++ и g++), но это было рассмотрено для С++ 0x, как предлагается в следующих черновиках:

Единственное изменение для С++ 0x, вероятно, что-то вроде:

inline template <typename T, size_t size>
constexpr size_t array_size(T (&p)[size])
{
   //return sizeof(p)/sizeof(p[0]) ;
   return size ; // corrected after Konrad Rudolph comment.
}

(обратите внимание на ключевое слово constexpr)

Изменить 2

Йоханнес Шауб - лит-бит предлагает другой, совместимый с С++ 03 способ сделать это. Я скопирую ссылку на источник здесь для справки, но зайдите на его ответ для полного примера (и повысьте его!):

template<typename T, size_t N> char (& array_size(T(&)[N]) )[N];

Используется как:

int p[] = { 1, 2, 3, 4, 5, 6 };
int u[sizeof array_size(p)]; // we get the size (6) at compile time.

Многие нейроны в моем мозгу жареные, чтобы я понял природу array_size (подсказка: это функция, возвращающая ссылку на массив из N символов).

: -)

Ответ 10

используя C путь (type) по сравнению с static_cast<type>(). см. там и там в stackoverflow для темы

Ответ 11

Локальное (автоматическое) объявление переменных

(Не верно, поскольку C99, как правильно указал Джонатан Леффлер)

В C вы должны объявить все локальные переменные в начале блока, в котором они определены.

В С++ возможно (и предпочтительно) отложить определение переменной до того, как оно будет использоваться. Позже предпочтительнее две основные причины:

  • Это увеличивает ясность программы (так как вы видите тип переменной, где он используется в первый раз).
  • Он упрощает рефакторинг (поскольку у вас небольшие когезионные фрагменты кода).
  • Это улучшает эффективность программы (поскольку переменные создаются именно тогда, когда они действительно нужны).

Ответ 12

В ответ на Alex Che и с честью на C:

В C99 текущая стандартная спецификация ISO для переменных C может быть объявлена ​​в любом месте блока, как и в С++. Следующий код действителен C99:

int main(void)
{
   for(int i = 0; i < 10; i++)
      ...

   int r = 0;
   return r;
}

Ответ 13

Я предлагаю нечто, что, возможно, совершенно очевидно, Пространства имен.

c переполненный глобальный охват:

void PrintToScreen(const char *pBuffer);
void PrintToFile(const char *pBuffer);
void PrintToSocket(const char *pBuffer);
void PrintPrettyToScreen(const char *pBuffer);

против.

С++ определяемые подразделы глобальной области действия, пространства имен:

namespace Screen
{
   void Print(const char *pBuffer);
}

namespace File
{
   void Print(const char *pBuffer);
}

namespace Socket
{
   void Print(const char *pBuffer);
}

namespace PrettyScreen
{
   void Print(const char *pBuffer);
}

Это немного надуманный пример, но способность классифицировать маркеры, которые вы определяете, в области, которые имеют смысл, препятствует запутанной цели функции с контекстом, в котором она вызывается.

Ответ 14

Следуя paercebal с использованием массивов переменной длины, чтобы обойти ограничение, функции которого не могут возвращать постоянные выражения, вот способ сделать это, в другим способом:

template<typename T, size_t N> char (& array_size(T(&)[N]) )[N];

Я написал это в некоторых моих других ответах, но он не подходит нигде лучше, чем в этот поток. Теперь, вот как это можно использовать:

void pass(int *q) {
    int n1 = sizeof(q); // oops, size of the pointer!
    int n2 = sizeof array_size(q); // error! q is not an array!
}

int main() {
    int p[] = { 1, 2, 3, 4, 5, 6 };
    int u[sizeof array_size(p)]; // we get the size at compile time.

    pass(p);
}

Преимущество над sizeof

  • Сбой для не-массивов. Не будет молча работать для указателей.
  • В коде укажет размер массива.

Ответ 15

std::copy vs. memcpy

Во-первых, есть проблемы с удобством использования:

  • memcpy принимает указатели void. Это избавляет от безопасности типов.
  • std::copy позволяет перекрывать диапазоны в определенных случаях (с std::copy_backward существующими для других перекрывающихся случаев), а memcpy никогда не разрешает это.
  • memcpy работает только с указателями, а std::copy работает с итераторами (из которых указатели - это особый случай, поэтому std::copy работает и с указателями). Это означает, что вы можете, например, std::copy элементов в std::list.

Несомненно, вся эта дополнительная безопасность и общность приходят по цене, верно?

Когда я измерил, я обнаружил, что std::copy имел небольшое преимущество перед memcpy.

Другими словами, кажется, что нет смысла использовать memcpy в реальном коде на С++.

Ответ 16

В интересах баланса этот пост имеет пример конструкции стиля C, которая иногда лучше, чем эквивалент стиля С++.

Ответ 17

Перегруженные функции:

C:

AddUserName(int userid, NameInfo nameinfo);
AddUserAge(int userid, int iAge);
AddUserAddress(int userid, AddressInfo addressinfo);

эквивалент/замена С++:

User::AddInfo(NameInfo nameinfo);
User::AddInfo(int iAge);
User::AddInfo(AddressInfo addressInfo);

Почему это улучшение:

Позволяет программисту выразить интерфейс таким образом, что понятие функции выражается в имени, а тип параметра выражается только в самом параметре. Позволяет вызывающему взаимодействовать с классом ближе к выражению понятий. Также, как правило, получается более сжатый, компактный и читаемый исходный код.

Ответ 18

iostreams

Отформатированный ввод-вывод может быть быстрее с использованием среды выполнения C. Но я не считаю, что низкоуровневый ввод-вывод (чтение, запись и т.д.) Медленнее с потоками. Возможность чтения или записи в поток без заботы, если другой конец - это файл, строка, сокет или какой-то определенный пользователем объект, невероятно полезен.

Ответ 19

В c большая часть вашей динамической функциональности достигается путем передачи указателей на функции. С++ позволяет создавать функциональные объекты, обеспечивая большую гибкость и безопасность. Я приведу пример, адаптированный из Stephen Dewhurst, отличный Общие знания С++

C Указатели функций:

int fibonacci() {
  static int a0 = 0, a1 =1; // problematic....
  int temp = a0;
  a0 = a1;
  a1 = temp + a0;
  return temp;
}

void Graph( (int)(*func)(void) );
void Graph2( (int)(*func1)(void), (int)(*func2)(void) ); 

Graph(fibonacci);
Graph2(fibonacci,fibonacci);

Вы можете видеть, что, учитывая статические переменные в функции fibonacci(), порядок выполнения Graph и Graph2() изменит поведение, не учитывая тот факт, что вызов Graph2() может иметь неожиданные результаты так как каждый вызов func1 и func2 даст следующее значение в ряду, а не следующее значение в отдельном экземпляре ряда относительно вызываемой функции. (Очевидно, вы могли бы экстернализировать состояние функции, но это было бы упущено, не говоря уже о запутывании пользователя и усложнении клиентских функций)

Объекты функции С++:

class Fib {
  public:
    Fib() : a0_(1), a1_(1) {}
    int operator();
  private:
    int a0_, a1_;
};
int Fib::operator() {
    int temp = a0_;
    a0_ = a1_;
    a1_ = temp + a0_;
    return temp;
}


template <class FuncT>
void Graph( FuncT &func );

template <class FuncT>
void Graph2( FuncT &func1, FuncT &func2); 

Fib a,b,c;
Graph(a);
Graph2(b,c);

Здесь порядок выполнения функций Graph() и Graph2() не изменяет результат вызова. Кроме того, при вызове Graph2() b и c поддерживаются отдельные состояния по мере их использования; каждый из них будет генерировать полную последовательность Фибоначчи индивидуально.

Ответ 20

new в С++ vs malloc в C. (для управления памятью)

новый оператор позволяет вызывать конструкторы классов, тогда как malloc не делает.

Ответ 21

Почти любое использование void*.