Конструкции С++, заменяющие конструкции C
После обсуждения с недавно появившимся разработчиком в моей команде я понял, что в С++ существуют привычки использовать конструкторы C, потому что они должны быть лучше (т.е. быстрее, более компактно, красивее, выберите причину).
Какими примерами стоит поделиться, показывая конструкции C, по сравнению с аналогичной конструкцией С++?
Для каждого примера мне нужно прочитать причины, по которым конструкция С++ так же хороша или даже лучше оригинальная конструкция C. Цель состоит в том, чтобы предложить альтернативы некоторым C-конструкциям, которые считаются несколько опасными/небезопасными в С++-коде (С++ 0x действительны только ответы принимаются до тех пор, пока они четко обозначены как С++ 0x).
В качестве примера я опубликую ниже ответ (встроенная инициализация структуры).
Примечание 1: Пожалуйста, один ответ за случай. Если у вас несколько случаев, отправьте несколько ответов
Примечание 2: Это не вопрос C. Не добавляйте тег "C" к этому вопросу. Это не должно стать борьбой между С++ и C. Только изучение некоторых конструкций подмножества C С++ и их альтернативы в других инструментариях С++ "
Примечание 3: Это не вопрос C-bashing. Мне нужны причины. Потрясающие, избивающие и недоказанные сравнения будут сбиты с толку. Упоминание функций С++ без эквивалента C можно рассматривать как не относящееся к теме: я хочу, чтобы рядом были добавлены функции C в отношении функции С++.
Ответы
Ответ 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*
.