1、初始化列表
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟 一个放在括号中的初始值或表达式。
class Date
{
public:
Date(int year,int month,int day)
:_year(year),_month(month),_day(day)
{
}
private:
int _year;
int _month;
int _day;
};
构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。
①每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
②类中包含以下成员,必须放在初始化列表位置进行初始化:
引用成员变量、const成员变量、自定义类型成员(且该类没有默认构造函数时)
③尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量, 一定会先使用初始化列表初始化。即使没有在初始化列表中初始化,系统也会默认在初始化列表处调用其默认的构造函数进行初始化。
④成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。例如,下图的例子中:
应选择D,即输出1和随机值,因为先对_a2初始化,这是_a1还未赋值,是个随机值。
2、explicit关键字
构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值 的构造函数,还具有类型转换的作用。
class Date
{
public:
Date(int year=2024)
:_year(year)
{
}
Date& operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022);
d1 = 2023;//发生类型转化
return 0;
}
这里d1=2023,实际编译器背后会用2023构造一个无名对象,最后用无名对象给d1对象进行赋值.
加上explicit 关键字修饰之后,就不会发生这种类型转换:
class Date
{
public:
explicit Date(int year=2024)
:_year(year)
{
}
Date& operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022);
d1 = 2023;//error C2679: 二元“=”: 没有找到接受“int”类型的右操作数的运算符(或没有可接受的转换)
return 0;
}
用explicit修饰构造函数,将会禁止构造函数的隐式转换,这样增强了代码可读性。
3、static总结
static的用法主要体现在两个方面:面向过程中的static和面向对象中的static。面向过程的static主要包括静态全局变量、静态局部变量和静态函数。面向对象的static主要包括静态成员变量、静态成员函数。
(1)面向过程
①静态全局变量:全局变量前添加static关键字,则该变量称为静态全局变量。
静态全局变量具有以下特点:
在全局数据中的变量如果没有显示的初始化会自动被程序初始化为0(这个特性非静态全局变量也有),而在函数体内声明的变量如果不显示初始化则会使一个随机值;
静态全局变量在声明它的整个文件中都是可见的,而在文件之外是不可见的;
静态全局变量在全局数据区分配内存;
其他文件中可以定义同名int型变量a,不会冲突;
如果将static去掉,那么:
全局变量默认是有外部连接性的,其作用域是整个工程,在一个文件内定义的全局变量可以通过包含其所在头文件或显示调用 extern关键字修饰全局变量的变量名声明来引用;
静态全局变量是显示调用static修饰的全局变量,其作用域只在声明此变量的文件中,其他文件即使使用extern关键字修饰其声明也不可使用。
②静态局部变量:局部变量前添加static关键字,则该变量称为静态局部变量。
在一个函数作用域内定义一个变量,每次运行到该函数时,系统会给局部变量分配内存。当函数结束时,该变量的内存会被系统回收至栈内存当中。也就是说,尽管出了函数的作用域,该静态局部变量的信息还被系统保存。如下的例子:
#include <iostream>
#include <stdio.h>
void Func()
{
static int a = 5;
printf("a = %d\n", a);
a++;
}
int main()
{
for (int i = 0; i < 5; i++)
{
Func(); //打印结果:5 6 7 8 9
}
system("pause");
return 0;
}
静态局部变量有如下特点:
内存存放在程序的全局数据区中;
静态局部变量在程序执行到该对象声明时,会被首次初始化。其后运行到该对象的声明时,不会再次初始化,这也是为什么上面程序测试函数每次输出的值都是递增的原因(只会被初始化一次);
如果静态局部变量没有被显式初始化,则其值会自动被系统初始化为0;
局部静态变量不能被其作用域之外的其他模块调用,其调用范围仅限于声明该变量的函数作用域当中;
③静态函数:函数返回类型前添加static关键字,则该变量称为静态函数。
特点:
作用域只在声明它的文件当中,不能被其他文件引用,其他文件可以定义同名的全局函数;
其他文件想要调用本文件的静态函数,需要显示的调用extern关键字修饰其声明;
(2)面向对象
①静态成员变量
特点:
静态数据成员的服务对象并非是单个类实例化的对象,而是所有类实例化的对象(这点可以用于设计模式中的单例模式实现);
静态数据成员必须显式的初始化分配内存,在其包含类没有任何实例化之前已经有内存分配,静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明;
静态数据成员与其他成员一样,遵从public,protected,private的访问规则;
静态数据成员内存存储在全局数据区,只随着进程的消亡而消亡;
class Test
{
public:
Test(int a, int b, int c) :
m_a(a),
m_b(b),
m_c(c)
{
m = a + b + c;
}
void Show()
{
cout << "m = " << m << endl;
}
private:
int m_a, m_b, m_c;
static int m;
};
int Test::m = 0; //初始化静态数据成员
int main()
{
Test ClassA(1, 1, 1);
ClassA.Show(); //输出: 3
Test ClassB(3, 3, 3);
ClassB.Show(); //输出: 9
ClassA.Show(); //输出: 9
return 0;
}
上面静态成员变量的初始化需要多加注意,即静态成员变量必须在类外定义。
②静态成员函数:类的成员函数返回类型之前添加static,此成员函数为静态成员函数。
特点:
1)静态成员函数比普通成员函数多了一种调用方式,即 类名::静态成员函数名();
2)在没有实例化的类对象的条件下可以调用类的静态成员函数;
3)静态成员函数中没有隐含的this指针,所以静态成员函数不可以操作类中的非静态成员(由于第二条可知,类的非静态成员是在类实例化后存在的,而类的成员函数可以在类没有实例化的时候调用,故不能操作类的非静态成员);
4、友元
友元分为:友元函数和友元类。
(1)友元函数
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在 类的内部声明,声明时需要加friend关键字。
例子:在对运算符>>和<<进行重载时,尤其是<<,在类中进行重载时,由于第一个参数一定是this指针,这就导致最后的形式会是实例对象<<cout,这与真正的情况顺序不一样。所以需要在类外进行运算符重载,但在类外又无法访问到类中的成员变量(受private限制),所以需要用到友元函数。
class Test
{
public:
Test(int a, int b, int c) :
m_a(a),
m_b(b),
m_c(c)
{
m = a + b + c;
}
void Show()
{
cout << "m = " << m << endl;
}
friend ostream& operator<< ( ostream& out, const Test& t);//没有该声明会报错
private:
int m_a, m_b, m_c;
static int m;
};
int Test::m = 0; //初始化静态数据成员
ostream& operator<< ( ostream& out,const Test& t)
{
out << t.m_a << t.m_b << t.m_c << endl;
return out;
}
友元函数的说明:
友元函数可访问类的私有和保护成员,但不是类的成员函数
友元函数不能用const修饰
友元函数可以在类定义的任何地方声明,不受类访问限定符限制
一个函数可以是多个类的友元函数
友元函数的调用与普通函数的调用原理相同
(2)友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
友元关系的特点:
①友元关系是单向的,不具有交换性。比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接 访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
②友元关系不能传递。如果C是B的友元, B是A的友元,则不能说明C是A的友元。
③友元关系不能继承。
5、内部类
如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类, 它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
注意:内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访 问外部类中的所有成员。但是外部类不是内部类的友元。
内部类的特点:
1.内部类可以定义在外部类的public、protected、private都是可以的。
2.注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
3.sizeof(外部类)=外部类,和内部类没有任何关系。
class AA
{
public:
void func()
{
//cout << _b1 << endl;//报错
//cout << BB::_b1 << endl;//报错
AA myA;
BB myB;
myB.fun(myA);
//cout << BB::_b1 << endl;//无法访问,因为AA不是BB的友元,BB是AA的内部类,那么BB是AA的友元
}
class BB {
public:
void fun(AA& myA)
{
cout << myA._a1 << endl;
cout << _a3 << endl;
}
private:
static int _b1;
};
private:
int _a1;
int _a2;
static int _a3;
};
int AA::_a3 = 0;
int AA::BB::_b1 = 1;
int main()
{
AA myAA;
myAA.func();
return 0;
}
6、匿名对象
匿名对象的特点是不用取名字,并且生命周期只在这一行,这一行结束后下一行就会自动调用析构函数。
(接上面的class AA):
int main()
{
/*AA myAA;
myAA.func();
cout << sizeof(AA);*/
AA().func();//匿名对象
return 0;
}
7、拷贝对象时的一些编译器优化
在传参和传返回值的过程中,一般编译器会做一些优化,减少对象的拷贝。
class A
{
public:
A(int a = 10)
:_a(a)
{
cout << "A(int a = 10)" << endl;
}
A(const A& aa)
:_a(aa._a)
{
cout << "A(A& aa)" << endl;
}
A& operator=(const A& aa)
{
_a = aa._a;
cout << "A& operator=(const A& aa)" << endl;
return *this;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
void f1(A aa)
{}
A f2()
{
A aa;
return aa;
}
int main()
{
// 传值传参
A aa1;//一次构造
f1(aa1);//一次拷贝构造
cout <<1<< endl;
// 传值返回
f2();
cout <<2<< endl;
// 隐式类型,连续构造+拷贝构造->优化为直接构造
f1(1);
// 一个表达式中,连续构造+拷贝构造->优化为一个构造
f1(A(2));
cout << 3<<endl;
// 一个表达式中,连续拷贝构造+拷贝构造->优化一个拷贝构造
A aa2 = f2();
cout << 4<<endl;
// 一个表达式中,连续拷贝构造+赋值重载->无法优化
aa1 = f2();
cout <<5<< endl;
return 0;
}
上述代码的运行结果为: