Конструктор по умолчанию с#. Конструкторы

26.08.2020 Интернет

class A { public: A():a(0),b(0) {} explicit A(int x): a(x), b(0) {} A(int x, int y): a(x), b(y) {} private: int a,b; };

Class A { public: explicit A(int x=0, int y=0): a(x), b(y) {} private: int a,b; };

Есть ли различия? Что лучше использовать?

2 ответа

Vlad from Moscow

Эти два объявления классов не эквивалентны.

Во втором объявлении класса конструктор объявлен со спецификатором функции explicit , а это ограничивает применение этого конструктора в различных ситуациях.

В первом же объявлении класса только конструктор преобразования объявлен со спецификатором функции explicit . А это означает, что другие конструкторы вы можете вызывать неявно.

То есть первое объявление предоставляет больше возможностей по использованию класса.

Рассмотрите следующую демонстрационную программу

#include struct A { explicit A(int x = 0, int y = 0) : x(x), y(y) {} int x; int y; }; struct B { B() : x(0), y(0) {} explicit B(int x): x(x), y(0) {} B(int x, int y): x(x), y(y) {} int x; int y; }; void f(const A &a) { std::cout << "a.x = " << a.x << ", a.y = " << a.y << std::endl; } void g(const B &b) { std::cout << "b.x = " << b.x << ", b.y = " << b.y << std::endl; } int main() { // f({}); // f({ 1, 2 }); g({}); g({ 1, 2 }); }

Ее вывод на консоль:

B.x = 0, b.y = 0 b.x = 1, b.y = 2

В этой программе два вызова функции f закомментированы, так как если их раскомментировать, то компилятор выдаст сообщение об ошибке.

Другое важное отличии состоит в том, что один класс имеет всего лишь один конструктор с заданной сигнатурой, а другой класс имеет три конструктора с различными сигнатурами.

Рассмотрите еще один демонстрационный пример

Struct A { explicit A(int x = 0, int y = 0) : x(x), y(y) {} int x; int y; }; struct B { B() : x(0), y(0) {} explicit B(int x): x(x), y(0) {} B(int x, int y): x(x), y(y) {} int x; int y; }; struct C { //friend A::A(); friend B::B(); }; int main() { }

Здесь в классе C вы можете объявить конструктор по умолчанию класса B в качестве друга класса С. Однако вы не можете сделать то же самое с конструктором по умолчанию класса A , чтобы объявить его другом класса C , так как конструктор по умолчанию в классе A имеет другую сигнатуру.

Вам уже придется писать

Struct C { friend A::A(int, int); };

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

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

Если рассматривать не конструкторы, а функции, то разница имеется еще более существенная.

Аргументы по умолчанию не влияют на тип функции. Поэтому, например, если вы объявили функцию как

Void f(int, int = 0);

то, несмотря на аргумент по умолчанию и того факта, что вы можете ее вызывать как

F(value);

тем не менее ее тип void(int, int) . А это в свою очередь означает, что вы не можете, например, написать

Void h(void f(int)) { f(10); } void f(int x, int y = 0) { std::cout << "x = " << x << ", y = " << y << std::endl; } // h(f);

так как параметр функции h имеет тип void(int) ., а у функции, используемой в качестве аргумента, тип void(int, int)

Если же вы объявите две функции вместо одной

Void h(void f(int)) { f(10); } void f(int x) { std::cout << "x = " << x << std::endl; } void f(int x, int y) { std::cout << "x = " << x << ", y = " << y << std::endl; }

то данный вызов

будет корректным, так как имеется функция с одним параметром.

IxSci

Различия уже объяснил @Vlad from Moscow, я лишь предложу, из двух вариантов в вопросе, третий вариант:

Class A { public: A():A(0, 0) {} explicit A(int x): A(x, 0) {} A(int x, int y): a{x}, b{y} {} private: int a,b; };

На мой взгляд именно этот вариант является лучшим, т.к. он имеет явный конструктор с одним аргументом, что является хорошей практикой и уберегает от некоторых ошибок. С другой стороны, explicit для конструкторов, у которых больше или меньше одного аргумента, на мой взгляд, является лишним. Т.к. случайно создать объект из более чем одного аргумента проблематично, а именно за этим мы и приписали explicit у одноаргументнова конструктора - защита от случайных ошибок.

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

Конструкторы

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

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

Конструктор – это специальная функция класса, предназначенная для инициализации полей класса в момент создания объекта этого класса.

Имя конструктора всегда совпадает с именем класса.

Конструкторов в классе может быть несколько или ни одного.

На конструкторы накладываются следующие ограничения:

1. Конструктор не может иметь возвращаемого значения даже void

2. Как следствие 1 нельзя использовать оператор return

3. Конструкторы нельзя объявлять виртуальными.

Конструкторы, в отличие от C++ в C# можно объявлять со спецификациями public, private и protected. Конструктор со спецификацией public автоматически (не явно) вызывается на этапе компиляции при создании экземпляра данного класса. Попытка вызвать конструктор явным образом вызовет ошибку компиляции. Конструкторы со спецификацией private и protected могут быть вызваны только с помощью других конструкторов, объявленных со спецификацией public. Различают следующие типы конструкторов:

1. Конструктор по умолчанию

2. Конструктор с аргументами

Конструктор, объявленный без аргументов, называется конструктором по умолчанию.

Если в классе программистом не определен конструктор по умолчанию, то, в отсутствии других конструкторов, компилятор создает его сам. Конструктор по умолчанию, созданный компилятором, инициализирует все поля класса следующим образом – числовым значениям – ноль, булевским переменным – false, для строк значение пустой ссылки – null.

namespace ConsoleApplication15

class CA // В классе СА нет явно объявленных конструкторов

public int x,y,z;

static void Main(string args)

CA obj=new CA();

Console.WriteLine("x ={0,2} y ={1,2} z ={2,2}",obj.x,obj.y,obj.z);

Результат работы программы:

x = 0 y = 0 z = 0

Добавим в определение класса СА конструктор по умолчанию и проследим, как изменится результат работы приложения:


public int x,y,z;

public CA()

Результат работы программы:

x = 3 y = 2 z = 5

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

1. Какие способы передачи параметров методам определены в C#?

2. Чем отличается передача параметров методам с помощью модификатора ref от передачи параметров методам с помощью модификатора out?

3. Можно ли с помощью оператора return вернуть из метода ссылку на объект?

4. Можно ли с помощью оператора return вернуть из метода массив?

5. Как организовать передачу методам переменного количества параметров?

6. Чем отличается обращение к статическим методам класса от обращения к не статическим методам?

7. К каким членам класса могут обращаться статические функции класса?

8. К каким членам класса могут обращаться не статические функции класса?

9. Что такое перегрузка функции?

10. Являются ли функции, заголовок которых отличается только типом возвращаемого значения, перегруженными?

11. Как отличить конструктор класса от других функций класса?

12. Для чего используются конструкторы?

13. В каком случае компилятор сам создает конструктор по умолчанию?

14. Сколько в классе может быть объявлено не статических конструкторов по умолчанию?

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

Вот простой пример, как вы ответите на следующий вопрос: сколько значимых типов из. NET Framework содержит конструкторы по умолчанию? Интуитивным ответом кажется "все ", и будете не правы, поскольку на самом деле, ни один из значимых типов. NET Framework не содержит конструктора по умолчанию .

Работа с массивами – это не единственное место, когда существующий конструктор значимого типа вызван не будет. Следующая таблица дает понять, когда такой конструктор будет вызван, а когда нет.
// Вызывается var cvt = new CustomValueType(); // Вызывается var cvt = Activator.CreateInstance(typeof(CustomValueType)); // Не вызывется var cvt = default(CustomValueType); static T CreateAsDefault() { return default(T); } // Не вызывается CustomValueType cvt = CreateAsDefault(); static T CreateWithNew() where T: new() { return new T(); } // Не вызывается!! CustomValueType cvt = CreateWithNew(); // Не вызывается var array = new CustomValueType;

Хочу обратить внимание на рассогласованность поведения в следующем случае: известно, что создания экземпляра обобщенного (generic) параметра происходит с помощью Activator.CreateInstance , именно поэтому при возникновении исключения в конструкторе пользователь обобщенного метода получит его не в чистом виде, а в виде TargetInvocationException . Однако при создании экземпляра типа CustomValueType с помощью Activator .CreateInstance наш конструктор по умолчанию будет вызван, а при вызове метода CreateWithNew и создания экземпляра значимого типа с помощью new T () – нет.

Заключение

Итак, мы выяснили следующее:
  1. Конструктором по умолчанию в языке C# называется инструкция обнуления значения объекта.
  2. С точки зрения CLR конструкторы по умолчанию существуют и язык C# даже умеет их вызывать.
  3. Язык C# не позволяет создавать пользовательские конструкторы по умолчанию для структур, поскольку это привело бы к падению производительности при работе с массивами и существенной путанице.
  4. Работая на языках, поддерживающих создание конструкторов по умолчанию, их объявлять все равно не стоит по тем же причинам, по которым они запрещены в большинстве языков платформы.NET.
  5. Значимые типы не так просты как кажется: помимо проблем с изменяемостью (мутабельностью) у значимых типов даже с конструкторами по умолчанию и то не все просто.

Функции-члены класса

#include

// содержит функции ввода-вывода

#include

// содержит функцию CharToOem

#include

// содержит математические функции

class Dot

// класс точки

// закрытые члены класса

char name ;

// имя точки

double x , y ;

// координаты точки

// открытые члены класса public :

// конструктор с параметрами

Dot (char Name , double X , double Y) { name = Name ; x = X ; y = Y ; }

void Print () ;

Здесь значение, переданное в конструктор при объявлении объекта A , используется для инициализации закрытых членов name , x и y этого объекта.

Фактически синтаксис передачи аргумента конструктору с параметрами является сокращенной формой записи следующего выражения:

Dot A = Dot ("A" , 3 , 4) ;

Однако практически всегда используется сокращенная форма синтаксиса, приведенная в примере.

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

Правила для конструкторов

Приведем правила, которые существуют для конструкторов:

конструктор класса вызывается всякий раз, когда создается объект его класса;

конструктор обычно инициализирует данные-члены класса и резервирует память для динамических членов;

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

класс может иметь несколько перегруженных конструкторов;

конструктор не может быть объявлен с модификатором const , volatile , static или virtual ;

если в классе не определен конструктор, компилятор генерирует конструктор по умолчанию , не имеющий параметров.

Правила для деструкторов

Для деструкторов существуют следующие правила: деструктор вызывается при удалении объекта;

деструктор обычно выполняет работу по освобождению памяти, занятой объектом;

деструктор имеет то же имя, что и класс, которому он принадлежит, с предшествующим символом ~ ; деструктор не может иметь параметров; деструктор не может возвращать значение; деструктор не наследуется; класс не может иметь более одного деструктора;

деструктор не может быть объявлен с модификатором const , volatile или static ;

если в классе не определен деструктор, компилятор генерирует деструктор по умолчанию .

Подчеркнем еще раз: конструкторы и деструкторы в C++ вызываются автоматически, что гаран-

тирует правильное создание и удаление объектов класса.

Список инициализации элементов

Обычно данные-члены класса инициализируются в теле конструктора, однако существует и другой способ инициализации – с помощью списка инициализации элементов. Список инициализации элементов отделяется двоеточием от заголовка определения функции и содержит данные-члены и базовые классы, разделенные запятыми. Для каждого элемента в круглых скобках непосредственно за ним указывается один или несколько параметров, используемых при инициализации. Синтаксис списка инициализации имеет вид:

ConstrName (parl , par2) : mem1 (parl) , mem2 (par2) , … { …}

В следующем примере для инициализации класса используется список инициализации элементов.

class Dot

char name ;

// имя точки

double x , y ;

// координаты точки

public:

// конструктор со списком инициализации

Dot (char Name) : name (Name) , x (0) , y (0) { }

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

Конструкторы по умолчанию

C++ определяет два специальных вида конструкторов: конструктор по умолчанию, о котором мы упоминали выше, и конструктор копирования. Конструктор по умолчанию не имеет параметров (или все его параметры должны иметь значения по умолчанию) и вызывается при создании объекта, которому не заданы аргументы. Следует избегать двусмысленности при вызове конструкторов. В приведенном ниже примере два конструктора по умолчанию являются двусмысленными:

class Т

public:

// конструктор по умолчанию

Т (int i = 0) ;

// конструктор с одним необязательным параметром

// может быть использован как конструктор по умолчанию

void main ()

// Использует конструктор T::T (int)

// Не верно; неоднозначность вызова Т::Т () или Т::Т (int = 0)

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

Конструкторы копирования

Конструктор копирования создает объект класса, копируя при этом данные из уже существующего объекта данного класса. В связи с этим он имеет в качестве единственного параметра константную ссылку на объект класса (const Т& ) или просто ссылку на объект класса (Т& ). Использование первого предпочтительнее, так как последний не позволяет копировать константные объекты.

рования по умолчанию. В C++ различают поверхностное и глубинное копирование данных.

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

В случае глубинного копирования происходит действительное копирование значений всех переменных из одной области памяти в другую. Если эти указатели или ссылки ссылаются на динамически распределенные объекты или на объекты, встроенные в копируемый объект, они будут недоступны в созданной копии объекта. Поэтому для классов, содержащих указатели и ссылки, следует включать в определение класса конструктор копирования, который будет осуществлять глубинное копирование, не полагаясь на созданный компилятором конструктор копирования по умолчанию. В этом конструкторе, как правило, выполняется копирование динамических структур данных, на которые указывают или на которые ссылаются члены класса. Класс должен содержать конструктор копирования, если он перегружает оператор присваивания.

Если в классе не определен конструктор, компилятор пытается сгенерировать собственный конструктор по умолчанию и, если нужно, собственный конструктор копирования. Эти сгенерированные компилятором конструкторы рассматриваются как открытые функции-члены. Подчеркнем, что компилятор генерирует эти конструкторы, если в классе не определен никакой другой конструктор.

Приведём пример использования конструктора копирования:

class Dot

// класс точки

// закрытые члены класса

char name ;

// имя точки

double x , y ;

// координаты точки

public:

// открытые члены класса

// конструкторы с параметрами

Dot (char Name , double X , double Y)

{ name = Name ; x = X ; y = Y ; }

Dot (char Name) : name(Name) , x (0) , y (0)

Dot (char Name , const Dot& A)

{ name = Name ; x = A.x ; y = A.y ; }

// конструктор копирования

Dot (const Dot& A)

{ name = (char ) 226 ; x = A.x ; y = A.y ; }

void Print () ;

// выводит на экран имя и координаты текущей точки

void main ()

Dot A ("A", 3 , 4) ;

// вызов конструктора Dot (char Name , double X , double Y)

// вызов конструктора Dot (char Name)

// выводит на экран:

Координаты точки т :

// вызов конструктора копирования Dot (const Dot& A)

// выводит на экран:

Координаты точки т :

Dot E ("E", A) ;

// вызов конструктора Dot (char Name , const Dot& A)

// выводит на экран:

Координаты точки E:

Конструктор копирования по умолчанию выполняет поверхностное копирование, которое в нашей задаче копирует не только координаты, но и имена точки. Хотя наш объект не содержит динамических чле-

Начнем с того, что когда мы создаем элементы (переменные) класса, мы не можем присвоить им значения в самом определении класса. Компилятор выдаст ошибку. Поэтому нам необходимо создавать отдельный метод (так называемую set-функцию) класса, с помощью которого и будет происходить инициализация элементов. При этом, если необходимо создать, к примеру, 20 объектов класса, то чтобы инициализировать элементы потребуется 20 раз вызвать set-функции.

Тут нам как раз сможет помочь конструктор класса. Кстати, конструктор (от слова construct — создавать) – это специальный метод класса, который предназначен для инициализации элементов класса некоторыми начальными значениями.

В отличии от конструктора, деструктор (от слова destruct — разрушать) — специальный метод класса, который служит для уничтожения элементов класса. Чаще всего его используют тогда, когда в конструкторе,при создании объекта класса, динамически был выделен участок памяти и необходимо эту память очистить, если эти значения уже не нужны для дальнейшей работы программы.

Важно запомнить:

  1. конструктор и деструктор, мы всегда объявляем в разделе public ;
  2. при объявлении конструктора, тип данных возвращаемого значения не указывается, в том числе — void !!!;
  3. у деструктора также нет типа данных для возвращаемого значения, к тому же деструктору нельзя передавать никаких параметров;
  4. имя класса и конструктора должно быть идентично;
  5. имя деструктора идентично имени конструктора, но с приставкой ~ ;
  6. В классе допустимо создавать несколько конструкторов, если это необходимо. Имена, согласно пункту 2 нашего списка, будут одинаковыми. Компилятор будет их различать по передаваемым параметрам (как при перегрузке функций). Если мы не передаем в конструктор параметры, он считается конструктором по умолчанию;
  7. Обратите внимание на то, что в классе может быть объявлен только один деструктор;

Сразу хочу привести пример, который доступно покажет, как работает конструктор:

# include using namespace std; class AB //класс { private: int a; int b; public: AB() //это конструктор: 1) у конструктора нет типа возвращаемого значения! в том числе void!!! // 2) имя должно быть таким как и у класса (в нашем случае AB) { a = 0;//присвоим начальные значения переменным b = 0; cout << "Работа конструктора при создании нового объекта: " << endl;//и здесь же их отобразим на экран cout << "a = " << a << endl; cout << "b = " << b << endl << endl; } void setAB() // с помощью этого метода изменим начальные значения заданные конструктором { cout << "Введите целое число а: "; cin >> a; cout << "Введите целое число b: "; cin >> b; } void getAB() //выведем на экран измененные значения { cout << "a = " << a << endl; cout << "b = " << b << endl << endl; } }; int main() { setlocale(LC_ALL, "rus"); AB obj1; //конструктор сработает на данном этапе (во время создания объекта класса) obj1.setAB(); //присвоим новые значения переменным obj1.getAB(); //и выведем их на экран AB obj2; //конструктор сработает на данном этапе (во время создания 2-го объекта класса) return 0; }

Результат работы программы:

Работа конструктора при создании нового объекта: a = 0 b = 0 Введите целое число а: 34 Введите целое число b: 67 a = 34 b = 67 Работа конструктора при создании нового объекта: a = 0 b = 0

Как видно из результата работы программы, конструктор срабатывает сразу, при создании объектов класса, поэтому, явно вызывать конструктор не нужно, он сам «приходит»:)

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

Рассмотрим еще один пример, это все та же программа, только в код внесены некоторые изменения. Тут же покажем принцип работы деструктора:

# include using namespace std; class AB //класс { private: int a; int b; public: AB(int A, int B) //эти параметры мы передадим при создании объекта в main { a = A;//присвоим нашим элементам класса значения параметров b = B; cout << "Тут сработал конструктор, который принимает параметры: " << endl;//и здесь же их отобразим на экран cout << "a = " << a << endl; cout << "b = " << b << endl << endl; } void setAB() { cout << "Введите целое число а: "; cin >> a; cout << "Введите целое число b: "; cin >> b; } void getAB() { cout << "a = " << a << endl; cout << "b = " << b << endl << endl; } ~AB() // это деструктор. не будем заставлять его чистить память, пусть просто покажет где он сработал { cout << "Тут сработал деструктор" << endl; } }; int main() { setlocale(LC_ALL, "rus"); AB obj1(100, 100); //передаем конструктору параметры obj1.setAB(); //присвоим новые значения переменным obj1.getAB(); //и выведем их на экран AB obj2(200, 200); //передаем конструктору параметры }

Смотрим результат работы программы :

Тут сработал конструктор, который принимает параметры: a = 100 b = 100 Введите целое число а: 333 Введите целое число b: 333 a = 333 b = 333 Тут сработал конструктор, который принимает параметры: a = 200 b = 200 Тут сработал деструктор Тут сработал деструктор

Деструктор срабатывает в тот момент, когда завершается работа программы и уничтожаются все данные. Мы его не вызывали – он сработал сам. Как видно, он сработал 2 раза, так как и конструктор. Уже от себя добавлю, что, в первую очередь, он удалил второй созданный объект (где a = 200, b = 200), а затем первый (где a = 100, b = 100). «Последним пришёл - первым вышел».