文章目录
- const成员
- 再谈构造函数
- 成员变量的定义
- 函数体内赋值
- 初始化列表
- 隐式类型转换
- explicit
- static成员
const成员
我们知道在调用类的成员函数时,会有一个默认的this指针且这个this指针时不可以被修改的,例如在日期类中,会有隐式的Date * const this;注意这里默认会在this前加const让指针的指向不被改变,那么如何让指针指向的成员变量不可以被改变呢?
class Date
{
public:
Date(const int year = 2003, const int month = 9, const int day = 19)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << ' ' << _month << ' ' << _day << endl;
}
private:
int _year = 1998;
int _month = 1;
int _day = 1;
};
int main()
{
Date d1(2000, 12);
const Date d2;
d1.Print();//d1.Print(&d1); Date* const this;
//d2.Print();//d2.Print(&d2); Date const* const this;
return 0;
}
为什么d2不可以调用成员函数,而d1可以?
本质原因是因为形参与实参不匹配,对象d2因为被const修饰所以成员变量不允许被改变,而在传过去时默认的this指针指向的成员变量是可以被改变,这就是权限的放大,所以不被允许。
void Print() const
{
cout << _year << ' ' << _month << ' ' << _day << endl;
}
在被调的成员函数后面加上修饰符const,这里的const修饰的是* this,也就由原来默认的
Date const this变成了Date const* const this;this和*this都不可以被改变。
只要函数内部this不发生改变,都可以在后面加上const对this进行修饰,这样const对象和普通对象都可以调用这个函数
再谈构造函数
前面我们提到默认构造函数,那么是不是每一个类中都要一个默认构造函数呢?当我们不显示的写构造函数时,编译器会默认生成一个无参的默认构造函数。默认构造函数就不是不需要参数就能够调用的,有三种一种就是我们不写构造函数时,编译器自己生成的,另一种是我们自己定义的构造函数,且没有参数,还有一种我们自己写的全缺省的构造函数,这三种都叫做,默认构造函数。而如果我们自己写的构造函数,就是必须手动传参才能够调用的构造函数,这种就不是默认构造函数,如果一个类中没有默认构造函数,那么我们就必须显示的调用我们自己写的那个构造函数,且要满足这个构造函数对参数的要求。
综上,我们可以只写构造函数不写默认构造函数,但是在初始化对象时,就必须要与我们自己写的构造函数的参数对应上,不过我们建议最好要写上,不然有时直接定义对象时,不传参就会出现没有合适的默认构造的情况。
成员变量的定义
首先我们要弄清楚类成员变量是在哪里定义和声明的
class Date
{
public:
Date(const int year,
const int month, const int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
一个成员变量只能在一个地方定义,并且只能初始化一次,不可以对同一个变量重复定义,但是当这个变量定义好之后,我们可以对它重复赋值,例如
int main()
{
int a = 5;
a = 6;
a = 7;
}
那么上面的日期类中成员变量是在哪里定义和声明的呢?
声明
private:
int _year;
int _month;
int _day = 1;
这里只是对成员变量的声明并不是定义,不占空间,这里的_day = 1,其中1是_day的缺省值,会在初始化列表中起作用,但如果形参day也有缺省值那么它就会被替代
函数体内赋值
Date(const int year, const int month, const int day)
{
_year = year;
_month = month;
_day = day;
}
构造函数大括号内是对成员变量赋值的地方,不是成员变量变量的地方
初始化列表
成员变量的定义是通过初始化列表对成员变量进行定义,并初始化,且有三种情况只能在初始化列表进行初始化。上面已经说过了一个变量只能初始化一次,如果在初始化列表中对成员变量初始化了,那么在大括号中的就是对它的赋值而不是初始化。
只能在初始化列表进行初始化的三种情况:
1.引用成员变量
2.const成员变量
3.自定义类型成员(且该类没有默认构造函数时)
对于内置类型可以在初始化列表进行定义,在大括号内进行初始化
为什么上面三种情况定义和初始化不可以分开呢?
前两种情况因为引用成员变量和const成员变量必须在定义时就对它进行初始化,第三种情况,是因为它是自定义类型,在定义时编译器默认就要对它进行初始化,而此时它却没有默认构造函数(因为你定义了其它普通的构造函数),所以你就必须显式的对你写的构造函数传参,来对这个自定义类型成员变量进行初始化。如果在有默认构造函数的情况下,我们就可以不显式的去对它进行初始化,因为编译器会自动调用默认构造函数对它进行初始化。而内置类型C++不对它进行处理,如果没有缺省值,在你不在初始化列表写的情况下,它默认只在初始化列表定义但不初始化。
总结:所有的成员类变量默认都要走一遍初始化列表,因为这里是成员变量定义的地方,但是有的成员变量必须在初始化列表进行初始化,有的则可以先定义在后面的大括号里进行初始化。那为什么有时候我们不写初始化列表,也可以直接在大括号里面进行赋值呢,比如上面的日期类,那是因为即使我们不显式的写出来,它也会默认的在初始化列表进行定义,并对自定义类型自动调用它的构造函数对它初始化,而另外三种必须在初始化列表进行初始化情况,如果我们选择直接在大括号内进行对它进行赋值的话就会出错,编译器不允许这种情况的存在。
初始化列表位于构造函数小括号和大括号之间,以冒号开头,不同变量之间用逗号分隔,初始化的值放在要初始化变量的括号里面
Date(const int year, const int month, const int day)
:_year(year)
,_month(month)
,_day(day)
{
}
三种必须显式在初始化列表进行初始化的情况
class A
{
public:
A(int a)
:_a(a)
{
}
private:
int _a;
};
class B
{
public:
B(int a, int& ref)
:_aobj(a)
,_ref(ref)
,_n(10)
{
}
private:
A _aobj; // 没有默认构造函数
int& _ref; // 引用
const int _n; // const
};
成员变量声明的顺序就是成员变量在定义时的顺序,与初始化列表中成员变量的顺序无关,所以我们在写成员初始化列表时,最好让初始化列表中变量的顺序与声明时的顺序保持一致,不然可能会出错。初始化列表是定义的地方,所以定义的顺序就是成员变量声明的顺序,如果你在大括号内进行初始化的话,是什么顺序就是什么顺序。
隐式类型转换
在之前我们知道在把一个变量拷贝值给另一个变量值时,其实不是直接拷贝而是会产生一个临时变量,例如一个double类型的变量赋给int类型的变量,这期间就会先产生一个int类型的临时变量,在把int类型的临时变量赋给int类型的变量,并且这个临时变量具有常性,而在赋值期间产生不同类型的临时变量的过程,就是隐式类型的转换,是由编译器主动完成的。
那么问题来了是否可以从一个内置类型转换成自定义类型呢?
//隐式类型转换
class A
{
public:
A(const int val)
:_val(val)
{
cout << "A(const int val)" << endl;
}
private:
int _val;
};
int main()
{
A a1(5);
A a2 = 10;
//A& a3 = 12;
return 0;
}
在a1中是直接调用构造函数,那么像a2的这种写法是否可以呢?它的具体过程又是什么样的呢?
a2的写法是可以的,不过它与a1略有不同,它包含隐式类型转换的过程,先由int类型的10通过调用构造函数,然后产生自定义类型A的临时变量,再由临时变量A拷贝构造给a2,实际上编译器对这种情况也会优化,优化为直接构造。那么你可能会有疑惑,我怎么知道它到底有没有进行隐式类型转换产生临时变量的过程呢?
例如上面a3的这种写法
A& a3 = 12;//错误的
这种写法是错误的,编译并不会通过,为什么呢?
其实就是因为中间产生了一个具有常性临时变量的原因,而这里的引用a3是变量,权限放大,自然不能绑定到常量上。
const A& a3 = 12;
如果加上了const让引用也变成常量,不可以被改变那么就可以,这里是权限的平移,所以这就很好的证明了这期间确实发生了隐式类型转换,先通过构造函数产生了一个临时变量。
隐式类型转换为自定义类型只适用于只有一个参数的自定义类型,如果有多个参数就不可以。
explicit
英文意思为显式,显性
在编写程序的过程中可能需要从一个类型转换成另一个类型这期间可能会发生隐式类型转换
例如显式的
int i = 5;
double d = (int)i;
单参构造函数,没有使用explicit修饰,具有类型转换作用。
为提高代码的可读性我们可以阻止隐式类型转换的发生,那么在转换为自定义类型的过程中我们怎么阻止隐式类型的转换呢?
explicit修饰构造函数,禁止类型转换。
static成员
有时候我们希望一个变量在函数调用后不被销毁,在下一次再调用这个函数时,其中一个变量不用再次被创建,且还保持为上一次的值,为了满足这些条件我们可以定义一个全局变量,但是全局变量也有一个坏处,就是谁都可以访问谁都可以改变,而当我们放在类中声明为静态成员变量时就会安全很多,不会被轻易访问并改变。
与普通成员变量不同的是静态成员变量属于这个类,而普通成员变量属于每一个类的对象。
class A
{
public:
A(int a, int b = 6)
:_a(a),
_b(b)
{
}
static int GetStatic()
{
return _c;
}
void Print()
{
cout << _a << '-' << _b << '-' << _c << endl;
}
private:
int _a = 4;
int _b = 3;
static int _c;//静态成员变量声明的地方
};
int A::_c = 1;//这里是类的静态成员变量定义的地方
int main()
{
A a1(4, 8);
a1.Print();
cout << a1.GetStatic() << endl;
cout << A::GetStatic() << endl;
return 0;
}
初始化列表是每一个对象的成员变量定义的地方,而静态成员变量_c是属于类的而不是属于对象,所以它不可以在初始化列表定义,也不会默认像普通成员变量一样走一遍初始化列表。C++规定类的静态成员变量在定义时可以通过访问限定符或者类域突破访问限定符,从而有一次可以在类外定义的机会,所以静态成员变量的定义可以在类外。类的静态成员变量规定在类外定义。
既然规定静态成员变量只有在定义时才可以通过类域或者访问限定符来访问,那么在定义之后我们要是想访问怎么访问呢?
我们可以再类中再定义一个函数来获取静态变量的值,但是对于访问静态成员变量我们一般定义一个静态成员函数与它对应,静态成员变量和静态成员函数这两个通常来说是配对使用的,因为静态成员变量属于类而不是属于哪一个对象,被存储在类中,而对于普通成员变量来说,是存储在每一个对象中的,因此每个对象的成员变量对应的值可能不同,所以在访问某一个对象的成员变量时就必须指明是哪一个对象的成员变量,对于静态成员变量它是属于这个类的,存储在类中,而不是存储在某一个对象中,它是被所有对象共享的,所以对于所有对象而言这个静态成员变量的值都是一样的,所以我们也可以不通过指明对象来访问。
静态成员函数形参中没有默认的形参this,所以可以直接通过类域来访问
静态成员函数的好处在于它不仅可以通过访问限定符来访问,也可以通过指定类域来访问,而普通成员函数只能通过访问限定符来进行访问,因为普通成员函数包含this指针,所以要给定确定的对象。
因为静态成员函数没有this指针,所以静态函数体内部不可以通过this来访问普通的成员变量,隐式写和显式写this都不行。