[Elementary C++] Classes and Objects (3)

Directory

  • 1. Let’s talk about the constructor again
    • 1.1 Initialization list
      • 1.1.1 How to write the initialization list
      • 1.1.2 Which members should use the initialization list?
    • 1.2 Characteristics of initialization list
      • 1.2.1 Solving queue problems
      • 1.2.2 The declaration order is the order of the initialization list
    • 1.3 explicit keyword
      • 1.3.1 The role of explicit keyword
  • 2. static members
    • 2.1 The concept of static members of a class
    • 2.2 How many objects are created in the class?
  • 3. Youyuan
    • 3.1 Concept
    • 3.2 Friend functions
    • 3.3 Friend classes
  • 4. Internal classes
  • 5. Some compiler optimizations when copying objects

1. Let’s talk about the constructor again

1.1 Initialization list

We have learned most of the constructor before, but not all of it. There is another very important thing – the initialization list.

1.1.1 How to write the initialization list

Initialization list: Begins with a colon, followed by a comma-separated list of data members, each “member variable” followed by an initial value or expression in parentheses.

 Date(int year, int month, int day)
:_year(year)
,_month(month)
,_day(day)
{<!-- -->
\t
}
//
Date d1(2023, 11, 9);

operation result:

Numeric values can also be included in parentheses (when no parameters are passed):

Date()
:_year(2023)
, _month(11)
, _day(9)
{<!-- -->

}

Supplement: The declaration of the default value is actually for the initialization list, but the constructor is not shown; if a constructor is written, the value defined in the constructor is used. The constructor has no parameters or the specific value is a random value. (The random value under VS is 0)

1.1.2 Which members should use initialization list

Let’s summarize it first and then analyze it one by one.

The members of the initialization list must be:
1. Reference member variables
2.const member variables
3.Custom-type member variables without default construction

public:
Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
,t(year)
,a(5)
{<!-- -->

}

private:
int _year;
int _month;
int _day;
int & t;//Reference member variable
const int a;//const member variable
};

operation result:

1There is a requirement for using references, that is, they must be initialized. There is no initialization in the above because these are all declarations. Next, we need to define this reference member variable. So where is the definition? Just in the initialization list.

2Const member variables are the same as references. They are only declared in the private area. If they are defined, they must be in the initialization list.

3When using the queue class in the previous article, we did not write the constructor of the queue class. If the built-in type declaration has a default value, use the default value. If not, it is a random value; the custom type member variable allows the compiler to automatically call its default value. structure.

Review what default construction is:
1. We do not display the write constructor automatically generated by the compiler by default.
2. No parameters passed
3. All default

If we want to control the number of parameters ourselves, that is to say, we need to pass parameters to the constructor of a custom type (or the constructor of a custom type is not a default constructor, we need Pass the parameters yourself, otherwise we neither display the constructor of the queue class nor construct its custom type as the default constructor, and the result will be a random value), you must display the initialization of the constructor , the constructor must have an initialization list.

in conclusion:
Reference member variables and const member variables must be initialized where they are defined, which is in the initialization list; custom type member variables do not write the constructor of this class, and the default constructor of this custom type member is automatically called; write the constructor of this class Function, let’s analyze it below~~

1.2 Characteristics of initialization list

1.2.1 Queue problem solving

We didn’t write the constructor of the queue class before, but we will write it this time. To initialize the member variables of the custom type in the constructor, we need to use the initialization list.

The following code:

class Stack
{<!-- -->
public:
Stack(int capacity = 3)
{<!-- -->
cout << "Stack(int capacity = 3)" << endl;
_a = (int*)malloc(sizeof(int) * capacity);
if (_a == NULL)
{<!-- -->
perror("malloc fail");
exit(-1);
}
_top = 0;
_capacity = capacity;
}
~Stack()
{<!-- -->
cout << "~Stack()" << endl;
free(_a);
_a = NULL;
_top = 0;
_capacity = 0;
}

private:
int* _a;
int _top;
int _capacity;
};
classQueue
{<!-- -->
public:
// Constructor of queue class
Queue()
:_st1()
,_st2()
,_size()
{<!-- -->

}
private:

Stack_st1;
Stack_st2;
int _size;
};
int main()
{<!-- -->
Queue q1;
return 0;
}

Who is the parameter? There are several situations:
1Only constructor, no parameters, no specific value after parentheses
View the allowed results through debugging:

We found that it is 0, 3, 0, 3, 0, so without passing any value, the initialization list has a random value for the built-in type (it becomes 0 under VS), and the custom type calls its default Construction

2No parameters, specific values after brackets

Queue()
:_st1(10)
, _st2(20)
, _size(30)
{<!-- -->

}

Debugging results:

3Some parameters are fully defaulted and have specific values after brackets

 Queue(Stack st1 = 44, Stack st2 = 66, int size = 88)
:_st1(1)
, _st2(2)
, _size(3)
{<!-- -->

}

Debugging results:

4The parameters are all defaults, and the parameters are after the brackets

 Queue(Stack st1 = 44, Stack st2 = 66, int size = 88)
:_st1(st1)
, _st2(st2)
, _size(size)
{<!-- -->

}

Debugging results:

5Transfer parameters yourself

 Queue(Stack st1 = 44, Stack st2 = 66, int size = 88)
:_st1(st1)
, _st2(st2)
, _size(size)
{<!-- -->

}
///
Queue q1(11, 77, 99);

Debugging results:

6There is no constructor, declare the default value

private:
Stack_st1 = 7;
Stack_st2 = 8;
int_size = 9;
};

Debugging results:

in conclusion:
1. When the constructor of a custom type member is not a default constructor, there are the following:
Pass parameters by yourself 5; if there are parameters, it will be full default, and the parameter after the brackets will be 4; if there are parameters, it will be full default, and there will be a specific value 3 after the brackets; if there are no parameters, there will be a specific value 2 after the brackets; Declare to default value 6. In short, there are parameters passed to the constructor of custom type members.
If there is a specific value, use the specific value. If there is no specific value, the parameter passed by yourself takes precedence, followed by the default value.
In order to facilitate parameter control, it is better to use 5.
2. When the constructor of a custom type member is the default constructor:
Only constructor, no parameters, no specific value 1
There is also a seventh type 7 Not writing a constructor (the way of writing the previous article)
There is actually no difference between the two, so we simply don’t write a constructor.

1.2.2 The declaration order is the order of the initialization list

Show it directly with an example:

class Date
{<!-- -->
public:
\t
Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
{<!-- -->

}
void Print()
{<!-- -->
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _day;//day is placed at the front
int _year;
int _month;
};
int main()
{<!-- -->
Date d1(2023, 11, 9);
d1.Print();
return 0;
}

Debugging results:

So the day is initialized first, followed by the year and month.

1.3 explicit keyword

1.3.1 The role of explicit keyword

Constructors with single parameters of built-in types have type conversion functions
Look at the following code:

class A
{<!-- -->
public:
A(int a)
{<!-- -->
_a = a;
}
void Print()
{<!-- -->
cout << _a << endl;
}
private:
int _a;
};
int main()
{<!-- -->
A aa(1);
aa = 3;
aa.Print();
return 0;
}

aa is a custom type, 3 is an integer, and the running result is:

If you do not want the conversion to occur, add explicit before the constructor, and the compiler will report an error.

Supplement: Adding the explicit keyword can prevent implicit type conversion, but it cannot prevent forced conversion.

Without explicit modification, multi-parameters and semi-default are also supported.

class Date
{<!-- -->
public:
Date(int year, int month = 2, int day = 3)
{<!-- -->
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{<!-- -->
Date d1 = (2023, 11, 10);
return 0;
}

Debugging results:

Note: The parentheses are comma expressions, only the last number is counted, the others are default values.

Modify it by changing the parentheses into curly braces and removing the default value:

Date d1 = {<!-- --> 2023, 11, 10 };

Debugging results:

2. static members

2.1 Concept of static members of classes

Class members declared as static are called static members of the class. Member variables modified with static are called static member variables; member functions modified with static are called static member functions.

2.2 How many objects are created in the class

Define a variable count for counting. Every time an object is created (constructed), or an object is copy-constructed, count + +. Let’s first use the global variable count to count and see what happens:
1

int count = 0;
class A
{<!-- -->
public:
A() {<!-- --> + + count; }
A(const A & amp; t) {<!-- --> + + count; }
~A() {<!-- --> }
private:

};

AFunc()
{<!-- -->
A aa;
return aa;
}
int main()
{<!-- -->
A aa;
Func();
cout << count << endl;
return 0;
}


The compiler reported an error, indicating that count was an ambiguous symbol, indicating a naming conflict.

Solution to naming conflicts – namespace
2

namespace yss
{<!-- -->
int count = 0;
}
class A
{<!-- -->
public:
A() {<!-- --> + + yss::count; }
A(const A & amp; t) {<!-- --> + + yss::count; }
~A() {<!-- --> }

private:
\t
};

AFunc()
{<!-- -->
A aa;
return aa;
}
int main()
{<!-- -->
A aa;
Func();
cout << yss::count << endl;
return 0;
}

operation result:

This result is indeed the answer we want, but there is a problem here: if there is one more class, the two classes cannot be distinguished using global variables. As a result, after counting one class, the count value of the other class will not be counted. Update, that is to say, the objects of the two classes are merged together.

Let’s change the way and define the member variables of the class
3

class A
{<!-- -->
public:
A() {<!-- --> + + count; }
A(const A & amp; t) {<!-- --> + + count; }
~A() {<!-- --> }

//private:
int count = 0;
};

AFunc()
{<!-- -->
A aa;
return aa;
}
int main()
{<!-- -->
A aa;
Func();
cout << aa.count << endl;
return 0;
}

Let’s comment out the private permissions for now and see what the results are:

Why is it 1? Because every time an object is created, count + 1, but when another object is created, count is cleared and then + 1. It means that each object in the class has a count, and the result we want is the number of all objects in the class, but each object has its own count.

What to do if you want all the objects in the class to have a count? Use static member variables and static modify count.
Static member variables must be initialized outside the class
4

class A
{<!-- -->
public:
A() {<!-- --> + + count; }
A(const A & amp; t) {<!-- --> + + count; }
~A() {<!-- --> }

//private:
static int count;
};
int A::count = 0;
AFunc()
{<!-- -->
A aa;
return aa;
}
int main()
{<!-- -->
A aa;
Func();
cout << aa.count << endl;
return 0;
}

operation result:

The result is no problem, but without private permissions, the encapsulation of the class cannot be very good.

Improvement: The value of count can be returned through a member function
5

class A
{<!-- -->
public:
A() {<!-- --> + + count; }
A(const A & amp; t) {<!-- --> + + count; }
~A() {<!-- --> }

int Getcount()
{<!-- -->
return count;
}
private:
static int count;
};
int A::count = 0;
AFunc()
{<!-- -->
A aa;
return aa;
}
int main()
{<!-- -->
A aa;
Func();
cout << aa.Getcount() << endl;
return 0;
}

operation result:

But what if we want to count the number of objects created by calling the Func function, and the objects instantiated in the main function are not counted?

Assuming that the Func function is called twice, don’t comment on the instantiation of the object, the result should be 5

int main()
{<!-- -->
A aa;
Func();
Func();
cout << aa.Getcount() << endl;
return 0;
}


Then I commented out the object and found that the compiler reported an error: Undefined identifier aa.

Can be solved using static member function
6

 static int Getcount()
{<!-- -->
return count;
}

Note: To access through Class name::static member

operation result:

Summarize:
Static member variables and static member functions are much like global variables and global functions, except that they are restricted by scope qualifiers and dot member operators
Replenish:
Static member functions cannot call non-static member functions, because static member functions do not have this pointers; non-static member functions can call static member functions of a class, because they belong to the same kind.

3. Friends

3.1 Concept

Friends provide a way to break through encapsulation, which sometimes provides convenience. However, friends will increase coupling and destroy encapsulation, so friends should not be used more than once.

Friends are divided into: Friend functions and friend classes

3.2 Friend functions

When a function is not a member function of a class but needs to be able to access member variables in the class, a friend function must be used.

Friend functions can directly access private members of a class. They are ordinary functions defined outside the class and do not belong to any class. However, they need to be declared inside the class. The friend keyword needs to be added when declaring.

class Date
{<!-- -->
public:
Date(int year, int month, int day)
{<!-- -->
_year = year;
_month = month;
_day = day;
}
//Friend function
friend ostream & amp; operator<<(ostream & amp; out, Date & amp; d);
private:
int _year;
int _month;
int _day;
};
ostream & amp; operator<<(ostream & amp; out, Date & amp; d)
{<!-- -->
out << d._year << "-" << d._month << "-" << d._day << endl;
return out;
}
int main()
{<!-- -->
Date d1(2023, 11, 10);
cout << d1;
return 0;
}

Features:
Friend functions can access private and protected members of a class, but not member functions of the class
Friend functions cannot be modified with const
Friend functions can be declared anywhere in a class definition and are not restricted by class access qualifiers.
A function can be a friend function of multiple classes
The principle of calling friend functions is the same as that of ordinary functions.

3.3 Friend Class

Create a time class and declare the date class as a friend class of the time class. Then the private member variables in the Time class can be directly accessed in the date class.

class Time
{<!-- -->
// Friend class
friend class Date;
public:
Time(int hour = 2020, int minute = 6, int second = 5)
: _hour(hour)
, _minute(minute)
, _second(second)
{<!-- -->}

private:
int _hour;
int _minute;
int _second;
};
class Date
{<!-- -->
public:
Date(int year = 2023, int month = 11, int day = 10)
: _year(year)
, _month(month)
, _day(day)
{<!-- -->}

void SetTimeOfDate(int hour, int minute, int second)
{<!-- -->
// Directly access private member variables of the time class
_t._hour = hour;
_t._minute = minute;
_t._second = second;
Print();
}
void Print()
{<!-- -->
cout << _t._hour << "-" << _t._minute << "-" << _t._second << endl;
}
private:
int _year;
int _month;
int _day;
Time_t;
};
int main()
{<!-- -->
Date d1;
d1.Print();
return 0;
}

Features:
Friendship is one-way and not exchangeable.
If you declare the Date class as its friend class in the Time class, you can directly access the private member variables of the Time class in the Date class, but you cannot access the private member variables of the Date class in the Time class.
Friend relationships cannot be transferred
If C is a friend of B and B is a friend of A, it cannot mean that C is a friend of A.
Friend relationships cannot be inherited

4. Internal classes

A class is defined inside another class. This internal class is called an internal class. The inner class is an independent class. It does not belong to the outer class, and the members of the inner class cannot be accessed through the objects of the outer class.

Inner classes are friend classes of outer classes. The inner class can access all members of the outer class through the object parameters of the outer class. But the outer class is not a friend of the inner class.

class A
{<!-- -->
public:
class B
{<!-- -->
public:
voidFunc()
{<!-- -->
cout << _a << endl;
}
};
private:
int _b;
private:
static int _a;
};
int A::_a = 10;
int main()
{<!-- -->
A::B b1;
b1.Func();
return 0;
}

Features:
Inner classes can be defined as public, protected, or private in external classes.
Note that inner classes can directly access static members in outer classes without requiring the object/class name of the outer class.
sizeof(external class)=external class, has nothing to do with internal class

5. Some compiler optimizations when copying objects

In the process of passing parameters and returning values, the compiler will generally do some optimizations to reduce object copies. Different compilers may optimize in different ways.

1Implicit type, continuous construction + copy construction->Optimized to direct construction

class A
{<!-- -->
public:
A(int a = 0)
:_a(a)
{<!-- -->
cout << "A(int a)" << endl;
}
A(const A & aa)
:_a(aa._a)
{<!-- -->
cout << "A(const A & amp; aa)" << endl;
}
private:
int _a;
};
voidFunc(A aa)
{<!-- -->
\t
}
int main()
{<!-- -->
Func(1);
return 0;
}


1 is an integer type. Implicit type conversion occurs by calling the constructor to generate a temporary variable. The temporary variable is then copied to the formal parameter and the copy constructor is called.

2In an expression, continuous construction + copy construction->optimized into one construction

class A
{<!-- -->
public:
A(int a = 0)
:_a(a)
{<!-- -->
cout << "A(int a)" << endl;
}
A(const A & aa)
:_a(aa._a)
{<!-- -->
cout << "A(const A & amp; aa)" << endl;
}
private:
int _a;
};
voidFunc(A aa)
{<!-- -->

}
int main()
{<!-- -->
Func(A(1));
return 0;
}


In an expression, A(1) calls the constructor to generate temporary variables. The temporary variables are then copied to the formal parameters and call the copy constructor, which is optimized into a one-time construction.

3In an expression, continuous copy construction + copy construction -> optimize a copy construction

class A
{<!-- -->
public:
A(int a = 0)
:_a(a)
{<!-- -->
cout << "A(int a)" << endl;
}
A(const A & aa)
:_a(aa._a)
{<!-- -->
cout << "A(const A & amp; aa)" << endl;
}
private:
int _a;
};
AFunc()
{<!-- -->
A aa;
return aa;
}
int main()
{<!-- -->
A aa2 = Func();
return 0;
}


Instantiate aa2 and call the construction once, enter the Func() function, create the aa object and call the construction once, and the two constructions are optimized into one construction; copy the local variable to the return value and call the copy construction, copy the return value to aa2 and then call the copy construction, twice The copy construction is optimized into a copy construction.

4In an expression, continuous copy construction + assignment overloading -> cannot be optimized

class A
{<!-- -->
public:
A(int a = 0)
:_a(a)
{<!-- -->
cout << "A(int a)" << endl;
}
A(const A & aa)
:_a(aa._a)
{<!-- -->
cout << "A(const A & amp; aa)" << endl;
}
private:
int _a;
};
AFunc()
{<!-- -->
A aa;
return aa;
}
int main()
{<!-- -->
A aa3;
aa3 = Func();
return 0;
}


Instantiate aa3 and call the constructor. At this time, aa3 already exists. The return value of calling Func() is assignment. The aa object is created in the Func() function and the constructor is called. The local variables are copied to the return value and the copy constructor is called, and finally the value is assigned.