Ответ 1
Я знаю, что вы говорите, что у вас есть хорошее понимание указателей и управления памятью, но я все равно хотел бы объяснить важный трюк. Как правило, в вашем пользовательском коде никогда не будет нового/удалить.
Каждое приобретение ресурсов (будь то блокировка синхронизации, соединение с базой данных или кусок памяти или что-либо еще, что необходимо получить и освободить) должна быть обернута в объект, чтобы конструктор выполнял сбор, а деструктор освобождает ресурс. Этот метод известен как RAII, и в основном это способ избежать утечек памяти. Привыкай к этому.
Стандартная библиотека С++, очевидно, использует это широко, поэтому вы можете понять, как она работает там. Прыгая немного в ваши вопросы, эквивалент List<T>
равен std::vector<T>
, и он использует RAII для управления своей памятью. Вы бы использовали его примерно так:
void foo() {
// declare a vector *without* using new. We want it allocated on the stack, not
// the heap. The vector can allocate data on the heap if and when it feels like
// it internally. We just don't need to see it in our user code
std::vector<int> v;
v.push_back(4);
v.push_back(42); // Add a few numbers to it
// And that is all. When we leave the scope of this function, the destructors
// of all local variables, in this case our vector, are called - regardless of
// *how* we leave the function. Even if an exception is thrown, v still goes
// out of scope, so its destructor is called, and it cleans up nicely. That
// also why C++ doesn't have a finally clause for exception handling, but only
// try/catch. Anything that would otherwise go in the finally clause can be put
// in the destructor of a local object.
}
Если бы мне пришлось выбрать один принцип, который программист на С++ должен изучать и обнимать, это выше. Пусть для вас действуют правила определения области и деструкторы. Они предлагают все гарантии, необходимые для написания безопасного кода.
Обработка строк:
std::string
твой друг. В C вы должны использовать массивы char (или char указателей), но это противно, потому что они не ведут себя как строки. В С++ у вас есть класс std::string, который ведет себя так, как вы ожидали. Единственное, что нужно иметь в виду, это то, что "мир привет" имеет тип char [12] и NOT std::string. (для совместимости с C), поэтому иногда вам нужно явно преобразовать строковый литерал (что-то заключенное в кавычки, например "hello world" ) в std::string, чтобы получить нужное поведение:
Вы все еще можете написать
std::string s = "hello world";
потому что строки C-стиля (такие как литералы, такие как "hello world" ) неявно конвертируются в std::string, но это не всегда работает: "hello" + "world" не будет компилироваться, потому что оператор + не определен для двух указателей. "hello worl" + 'd', однако, скомпилируется, но он не будет делать ничего разумного. Вместо добавления char в строку, он примет интегральное значение char (которое получает повышение до int) и добавляет его к значению указателя.
std::string ( "hello worl" ) + "d" делает так, как вы ожидали, потому что левая сторона уже есть std::string, а оператор добавления перегружен для std::string, чтобы сделать то, что вы 'd ожидать, даже если правая сторона char * или один символ.
Последняя заметка о строках: std::string использует char, который является однобайтовым типом данных. То есть, он не подходит для текста в Юникоде. С++ предоставляет широкий тип символов wchar_t, который является 2 или 4 байтами, в зависимости от платформы, и обычно используется для текста в Юникоде (хотя ни в одном случае стандарт С++ не указывает набор символов). И строка wchar_t называется std:: wstring.
Библиотеки:
Они не существуют, в основном. Язык С++ не имеет понятия о библиотеках, и к этому привыкает. Он позволяет # включать другой файл (обычно файл заголовка с расширением .h или .hpp), но это просто текстовая копия/вставка. Препроцессор просто объединяет два файла, что приводит к тому, что называется единицей перевода. Несколько исходных файлов, как правило, содержат одни и те же заголовки и работают только при определенных обстоятельствах, поэтому этот бит является ключом к пониманию модели компиляции С++, которая, как известно, является изворотливой. Вместо того, чтобы компилировать кучу отдельных модулей и вызывать какие-то метаданные между ними, как компилятор С#, каждая единица перевода компилируется изолированно, а результирующие объектные файлы передаются в компоновщик, который затем пытается объединить общие биты (если в нескольких единицах перевода включен один и тот же заголовок, вы по существу дублируете код в единицах перевода, поэтому компоновщик объединяет их обратно в одно определение);)
Конечно, для написания библиотек существуют специфичные для платформы способы. В Windows вы можете сделать .dll или .libs, с той разницей, что .lib связан с вашим приложением, а .dll - это отдельный файл, который вам нужно связать с вашим приложением, как и в .NET. В Linux эквивалентные типы файлов -.so и .a, и во всех случаях вам также необходимо предоставить соответствующие файлы заголовков, чтобы люди могли разрабатывать их против ваших библиотек.
Преобразования типов данных:
Я не уверен точно, что вы ищете там, но один момент, который я чувствую, имеет большое значение, так это то, что "традиционная" трансляция, как в следующем, плохо:
int i = (int)42.0f;
Для этого есть несколько причин. Во-первых, он пытается выполнить несколько различных типов приведения в порядок, и вы можете быть удивлены тем, какой компилятор заканчивается. Во-вторых, его трудно найти в поиске, а в-третьих, это не уродливо. Лучше всего избегать броска, а на С++ они немного уродливы, чтобы напомнить вам об этом.;)
// The most common cast, when the types are known at compile-time. That is, if
// inheritance isn't involved, this is generally the one to use
static_cast<U>(T);
// The equivalent for polymorphic types. Does the same as above, but performs a
// runtime typecheck to ensure that the cast is actually valid
dynamic_cast<U>(T);
// Is mainly used for converting pointer types. Basically, it says "don't perform
// an actual conversion of the data (like from 42.0f to 42), but simply take the
// same bit pattern and reinterpret it as if it had been something else). It is
// usually not portable, and in fact, guarantees less than I just said.
reinterpret_cast<U>(T);
// For adding or removing const-ness. You can't call a non-const member function
// of a const object, but with a const-cast you can remove the const-ness from
// the object. Generally a bad idea, but can be necessary.
const_cast<U>(T);
Как вы заметите, эти приведения гораздо более конкретны, а это значит, что компилятор может дать вам ошибку, если приведение недействительно (в отличие от традиционного синтаксиса, где он просто попытается выполнить любой из вышеперечисленных бросков, пока не найдет тот, который работает), и он большой и многословный, позволяющий вам искать его и напоминает вам, что их следует избегать, когда это возможно.;)
Стандартная библиотека:
Наконец, возвращаясь к структурам данных, приложим некоторые усилия для понимания стандартной библиотеки. Он небольшой, но удивительно универсальный, и как только вы узнаете, как его использовать, вы окажетесь в гораздо лучшем положении.
Стандартная библиотека состоит из нескольких довольно четких строительных блоков (библиотека со временем накапливается. Части ее были перенесены с C. Библиотека потоков ввода-вывода принимается из одного места, а классы-контейнеры и связанные с ними функциональность принимается из совершенно другой библиотеки и отличается значительно различной: последние являются частью того, что часто называют STL (стандартная библиотека шаблонов). Строго говоря, это имя библиотеки, которая слегка изменена, принятой в стандартную библиотеку С++.
STL является ключом к пониманию "современного С++". Он состоит из трех столбов, контейнеров, итераторов и алгоритмов. Вкратце, контейнеры выставляют итераторы, а алгоритмы работают над парами итераторов.
Следующий пример принимает вектор int, добавляет 1 к каждому элементу и копирует его в связанный список, только для примера:
int add1(int i) { return i+1; } // The function we wish to apply
void foo() {
std::vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(5); // Add the numbers 1-5 to the vector
std::list<int> l;
// Transform is an algorithm which applies some transformation to every element
// in an iterator range, and stores the output to a separate iterator
std::transform (
v.begin(),
v.end(), // Get an iterator range spanning the entire vector
// Create a special iterator which, when you move it forward, adds a new
// element to the container it points to. The output will be assigned to this
std::back_inserter(l)
add1); // And finally, the function we wish to apply to each element
}
Вышеприведенный стиль немного привыкает, но он чрезвычайно мощный и лаконичный. Поскольку функция преобразования является шаблоном, она может принимать любые типы в качестве входных данных, если они ведут себя как итераторы. Это означает, что функция может использоваться для объединения любых типов контейнеров или даже потоков или чего-либо еще, что может быть выполнено итерацией до тех пор, пока итератор предназначен для совместимости с STL. Нам также не нужно использовать пару begin/end. Вместо конечного итератора мы могли бы пройти один, указывая на третий элемент, и тогда алгоритм остановился бы там. Или мы могли бы написать пользовательские итераторы, которые пропускали все остальные элементы или что-то еще, что нам нравилось. Вышеприведенный пример является основным примером каждого из трех столпов. Мы используем контейнер для хранения наших данных, но алгоритм, который мы используем для его обработки, фактически не должен знать о контейнере. Он просто должен знать о диапазоне итераторов, на котором он должен работать. И, конечно, каждый из этих трех столпов можно расширить, написав новые классы, которые затем будут работать плавно вместе с остальной частью STL.
В некотором смысле это очень похоже на LINQ, поэтому, поскольку вы пришли из .NET, вы, вероятно, можете увидеть некоторые аналоги. STL-аналог немного более гибкий, хотя ценой немного более странного синтаксиса.:) (Как уже упоминалось в комментариях, он также более эффективен. В целом, для алгоритмов STL есть нулевые накладные расходы, они могут быть столь же эффективными, как и ручные кодированные контуры. Это часто бывает неожиданным, но возможно, потому что все соответствующие типы известны в compile-time (что является требованием для работы шаблонов), а компиляторы С++ имеют тенденцию к активному встраиванию.)