c++类和对象
初始化结束才会加各种关键字修饰
1.再谈构造函数
全局对象在调用main前就会调用构造,即程序在运行前会先完成全局的准备工作,再执行main函数。
静态修饰的成员,执行到static这一步时才会移动到全局;
1.1构造函数体赋值
在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。虽然构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化,构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为**初始化只能初始化一次,而构造函数体内可以多次赋值**。
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
1.2初始化列表
约定,在初始化列表进行初始化,函数体内进行自动调用行为,如:函数体进行malloc并语法检查,malloc开辟空间不会初始化;动态开辟二维数组,使用初始化列表无法完成,需要先开辟指针数组,在循环开辟多个数组;
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量" 后面跟一个放在括号中的初始值或表达式。
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
1.2.1初始化列表的特性
1.每个成员变量在初始化列表中最多只能出现一次(初始化只能初始化一次) ;
2.类中包含以下成员,必须放在初始化列表位置进行初始化: 引用成员变量 ;const成员变量 ;自定义类型成员(且该类没有默认构造函数时);
class A
{
public:
A(int a)
:_a(a)
{}
private:
int _a;
};
class B
{
public:
B(int a, int ref)
:_aobj(a)
,_ref(ref)//引用和const对象必须在定义时候初始化
,_n(10)
{}
private:
//这里仅仅是变量声明,对象实例化是对象整体的定义,对象成员的定义在初始化列表,缺省值会给到初始化列表
A _aobj; // 没有默认构造函数
int& _ref; // 引用,不能引用局部变量,所以初始化时不传常量,用引用传变量。
const int _n; // const
};
3.尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使
用初始化列表初始化;
4. 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关;
1.3explicit关键字
1.3.1自定义类型的隐式类型转换
显式或隐式类型转换都会产生临时对象;
构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函
数,还具有类型转换的作用。自定义类型具备这种特征才能进行隐式类型转换。
隐式类型转换是为了实现string a = “ll”,直接用字符串构造a。
class A
{
public:
A(int a/*, int b*/ )
: a_(a)
//, b_(b)
{
cout << "构造" << endl;
}
A(const A& args)
{
cout << "拷贝构造" << endl;
a_ = args.a_;
}
~A() {}
private:
int a_ = 0;
};
int main()
{
A aa(1);
A aa0 = aa;//1.此处时拷贝构造
A aa1 = 1;//2.这里时发生隐式类型转换,先生成临时对象A tmp(1),然后aa1(tmp),但是编译器会直接优化成构造,即编译器会对连续的构造进行优化;临时对象的构造是将1作为tmp第一个参数传入,若有多参数,则其他参数都有缺省值;
return 0;
}
int main()
{
A b(1);
const A a = b;//初始化结束,构造完才用const修饰
return 0;
}
1.3.2explicit关键字的作用
在这种特殊构造函数前加上explicit关键字修饰,即可不发生隐式类型转换。
2.static成员
1.静态成员变量私有声明,静态成员函数公有声明;
2.静态成员变量就和函数一样,无需实例化就已经先存在了,并且不存放在对象内;
2.1概念
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的
成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化;
注意:只需要在类内声明静态成员,且仅在声明时用static修饰即可,类外定义时,无需用static修饰;
2.2普通成员变量和静态成员变量的区别
1.普通成员变量独属于类对象,静态成员变量属于类,不独属于类对象,存储在静态区;
2.静态成员变量在声明时加缺省值必须用const修饰,因为类内声明static加缺省值,会被识别为成员常量,所以有缺省值且用static修饰就必须用const修饰,如:static const size_t npos = -1;
3.普通成员变量再初始化列表初始化,而静态成员变量在类外初始化,定义;
2.3普通成员函数和静态成员函数的区别
1.静态成员函数没有this指针,直接指定类域访问;
2.静态成员函数只能访问静态成员变量,普通成员函数都可以访问;
3.普通成员函数只能对象.和指针->访问,而静态成员函数对象. 类域:: 指针->都可以
即静态函数提供了一种类域访问,但需要注意访问限定;
4.静态函数不可以调用非静态函数,没有this指针;
2.4static成员的应用
1.静态成员变量,如:1.用静态成员记录程序中实时存在的类对象数量,如果用全局变量,可以随便修改,用类使用静态成员,则类外就无法随便修改;2.创建对象实现累加;
2.静态成员函数,如:将构造函数私有化,用静态成员函数来进行栈/堆对象的获取,单例模式;
3.友元
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
3.1友元函数
现在尝试去重载operator<<,然后发现没办法将operator<<重载成成员函数。因为cout的输出流对象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作数了。但是实际使用 中cout需要是第一个形参对象,才能正常使用。所以要将operator<<重载成全局函数。但又会导致类外没办 法访问成员,此时就需要友元来解决。
注意:
1.友元函数可访问类的私有和保护成员,但不是类的成员函数 ;
2.友元函数不能用const修饰,因为const修饰的时*this,而友元函数不是类的成员函数;
3.友元函数可以在类定义的任何地方声明,不受类访问限定符限制;
4.一个函数可以是多个类的友元函数 ;
5.友元函数的调用与普通函数的调用原理相同;
6.友元声明在类的public、protected、private都是可以的;
3.2友元类
和友元函数类似,也是在想访问的类里面加上声明。
1.友元关系是单向的,不具有交换性;
2.友元关系不能传递;
3.友元关系不能继承;
4.内部类
概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。内部类是外部类的友元类。
4.1特性
1.内部类定义在外部类的public、protected、private都是可以的;
2.注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名;
3.内部类是外部类的友元,内部类的this != 外部类this,即不是同一个对象。用类域访问,没创建实际对象,所以要用对象.访问,而static变量是共享的,可以用类域访问,但内部类在外部类的类域里面,可以看见类域里的变量,不需要类域访问;
4.sizeof(外部类)=外部类,和内部类没有任何关系;
5.内部类直接在外部类里定义并不是声明,所以受访问限定符的限制;
class A
{
private:
static int k;
int h;
public:
class B // B天生就是A的友元
{
public:
void foo(const A& a)
{
cout << k << endl;//OK
cout << a.h << endl;//OK
}
private:
int x;
};
};
int A::k = 0;
int main()
{
cout << sizeof(A);//4,内部类和外部类,没有属于关系
return 0;
}
4.2应用
1.私有内部类如:迭代类;
2.内部类和外部类看到同一个变量,即静态变量;
5.匿名对象
5.1匿名对象的使用
class A
{
public:
A(int a = 0, int b = 0) :a_(a), b_(b) {}
~A() {}
void print(){}
private:
int a_;
int b_;
};
int main()
{
A a;//有名对象
A();//匿名对象
return 0;
}
5.2匿名对象的使用场景
1.匿名对象调用函数,如:A( ).print( ) ;
2.用匿名对象给缺省值,如:void func(string str=string());
3.用匿名对象进行初始化,如:
class A
{
public:
A(int aa = 0, int bb = 0) :a(aa), b(bb) {}
~A() {}
private:
int a;
int b;
};
int main()
{
A a[2]{ {},{} };
A* ptr = new A[2]{ A(1), A(1) };
return 0;
}
5.3匿名对象特性
1.有名对象生命周期是余部作用域,而匿名对象的生命周期是其实例化所在行;
2.匿名对象具有常性,匿名对象不等于临时对象;
3.对匿名对象常引用之后,匿名对象就延长了生命周期,如传参时传递匿名对象;
4.匿名对象对于自定义类型会调用默认构造,内置类型,int 是0,double是0.000000,char是’\0’,指针类型是空指针。
void push_back(const string& str)
{
cout << str << endl;
}
class A
{
public:
A test(){}
private:
int a;
};
int main()
{
push_back(string("123"));
//push_back("123");
A a;
const A& ret = A().test();//这里返回的是临时变量,但延长了生命周期,匿名对象也类似
return 0;
}
5.4匿名对象和临时对象的区别
- 生命周期:隐式类型转换产生的临时对象通常只在表达式执行期间存在,而匿名对象可能具有更长的生命周期,取决于它们是如何被使用的。
- 目的:隐式类型转换产生的临时对象是为了满足某种类型转换的要求而创建的,而匿名对象则可能是为了初始化、返回或其他目的而创建的。
- 用户控制:用户可以通过定义转换构造函数或转换运算符来控制隐式类型转换的行为,但无法直接控制匿名对象的创建。
- 可见性:由于它们是匿名的,所以这两种对象都不能通过名称直接访问。然而,你可以通过引用或指针来延长临时对象的生命周期,使其能够在表达式结束后仍然存在。
总的来说,隐式类型转换产生的临时对象和匿名对象都是在表达式中创建的没有显式名称的对象,但它们的生命周期、用途和用户控制方面有所不同
6.编译器对临时对象的优化
编译器会对同一行所进行的连续的构造,优化为一次最外层的构造。
建议能一行构造就一行构造。
A Func()
{
A a;
return a;
}
int main()
{
A ra = Func();//应该进行两次构造,先临时对象的拷贝构造,然后ra的拷贝构造;编译器优化为ra直接拷贝构造
}