目录
一、理解类与对象
原文链接:(1条消息) 类与对象的理解_Coder Xu的博客-CSDN博客_类和对象的理解
类的定义:
类(class):是构造 对象 的模板或蓝图,类也是一组相关属性和行为的集合。
属性(成员变量):就是该事物的状态信息(如同手机的颜色,材料)
行为(成员方法):就是该事物能够做什么(如同手机的打电话,发短信功能)
类就如下图的手机图纸,它是抽象的,而不是真实存在的。
对象的定义:
对象:对象是类的实例。由于对象是根据类创建出来的,所以对象具备类中的属性和行为。
对象就如下图真实存在的手机,它是真实的。
类和对象的关系:类是对象的蓝图(模板),对象是类的实体。
二、再谈构造函数
2.1 构造函数体赋值
在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化,
构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。
2.2 初始化列表
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
初始化列表是对象的成员定义的地方。
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
【注意】
1). 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
2). 类中包含以下成员,必须放在初始化列表位置进行初始化:
引用成员变量
const成员变量
自定义类型成员(且该类没有默认构造函数时)自己写了就不会有默认的。
class A
{
public:
A(int a)
:_a(a)
{}
private:
int _a;
};
class Date
{
public:
//只能在初始化列表初始化
Date(int year, int n, int ref, int a)
:_n(n)
,_ref(ref)
,_aa(a)
{
_year = year;
}
private:
//声明
//可以在定义时初始化;也可以定义时不初始化,后面再赋值修改
int _year;
//只能在定义的时候初始化
const int _n;
int& _ref;
A _aa;//自定义类型,A类,对象_aa
//且该类没有默认构造函数时
};
笔试题:
有一个类A,其数据成员如下: 则构造函数中,成员变量一定要通过初始化列表来初始化的是:( )
class A {
...
private:
int a;
public:
const int b;
float* &c;
static const char* d;
static double* e;
};
A.a b c
B.b c
C.b c d e
D.b c d
E.b
F.c
A.a是不同数据成员,可以通过构造函数进行赋值
B.正确,常量以及引用只能通过初始化列表初始化
C.d,e是静态成员,只能在类外初始化
D.d是静态成员,只能在类外初始化
E.b常量只能通过初始化列表初始化,但不是最佳答案
F.c引用只能通过初始化列表初始化,但不是最佳答案
3). 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
class A
{
public:
//A(int x)
//{
// _x = x;/*“A”: 没有合适的默认构造函数可用*/
//}
A(int a)
:_a(a)
{
cout << "sss" << endl;
}
private:
int _a;
};
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1 ,int a = 1)
:_aa(a)
{
_year = year;
_month = month;
_day = day;
cout << "bbbb" << endl;
}
//初始化列表可以认为就是对象成员变量定义的地方
//Date(int year, int month, int day, int a)
// : _year(year)
// , _month(month)
// , _aa(a)
// //, _day(day)
//{
// _day = day;
//}
private:
int _year;//声明
int _month;
int _day;
A _aa;//自定义类型的成员
};
int main()
{
Date d1(2022, 5, 12, 20 );//对象定义
//Date d2;
return 0;
}
4). 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
class A
{
public:
A(int a)
:_a1(a)
,_a2(_a1)
{}
void Print()
{
cout<<_a1<<" "<<_a2<<endl;
}
private:
int _a2;
int _a1;
};
int main()
{
A aa(1);
aa.Print();
}
初始化顺序由定义类时的声明顺序决定,所以先初始化_a2,由于初始化_a2时_a1还未初始化,所以为随机值。
所以如果将声明顺序进行交换就可以输出我们预想的值:
#include<iostream>
using namespace std;
class A
{
public:
A(int a)
:_a1(a)//_a1 = 1
, _a2(_a1)//_a2 = _a1
{}
void Print()
{
cout << _a1 << " " << _a2 << endl;
}
private:
int _a1;
int _a2;
};
int main()
{
A aa(1);//类的实例化aa,初始化为1
aa.Print();
return 0;
}
2.3 explicit关键字
C++中的explicit关键字只能用于修饰只有一个参数的类构造函数, 它的作用是表明该构造函数是显示的, 而非隐式的, 跟它相对应的另一个关键字是implicit, 意思是隐藏的,类构造函数默认情况下即声明为implicit(隐式).
构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用。
class Date
{
public:
// 1. 单参构造函数,没有使用explicit修饰,具有类型转换作用
// explicit修饰构造函数,禁止类型转换---explicit去掉之后,代码可以通过编译
explicit Date(int year)
:_year(year)
{}
/*
// 2. 虽然有多个参数,但是创建对象时后两个参数可以不传递,没有使用explicit修饰,具有类型转
换作用
// explicit修饰构造函数,禁止类型转换
explicit Date(int year, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
*/
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
private:
int _year;
int _month;
int _day;
};
void Test()
{
Date d1(2022);
// 用一个整形变量给日期类型对象赋值
// 实际编译器背后会用2023构造一个无名对象,最后用无名对象给d1对象进行赋值
d1 = 2023;
// 将1屏蔽掉,2放开时则编译失败,因为explicit修饰构造函数,禁止了单参构造函数类型转换的作用
}
explicit关键字只对有一个参数的类构造函数有效, 如果类构造函数参数大于或等于两个时, 是不会产生隐式转换的, 所以explicit关键字也就无效了。但是, 也有一个例外, 就是当除了第一个参数以外的其他参数都有默认值的时候, explicit关键字依然有效, 此时, 当调用构造函数时只传入一个参数, 等效于只有一个参数的类构造函数。
上述代码可读性不是很好,用explicit修饰构造函数,将会禁止构造函数的隐式转换。
三、static成员
static 成员变量和普通 static 变量一样,编译时在静态数据区分配内存,到程序结束时才释放。这就意味着,static成员变量不随对象的创建而分配内存,也不随对象的销毁而释放内存。而普通成员变量在对象创建时分配内存,在对象销毁时释放内存。
静态局部变量的特殊之处在于,函数被调用N次,初始化语句只会执行一次,之后则会取在内存静态区中储存的上次函数调用结束时该静态变量的值,而不是初始化语句。
//特殊之处的举例说明
#include<iostream>
using namespace std;
void Static()
{
static int n = 1;
n *= 2;
cout<<"静态变量:"<<n<<endl;
}
void Auto()
{
int n = 1;
n *= 2;
cout<<"普通变量:"<<n<<endl;
}
int main()
{
Static();
Static();
Static();
Static();
Auto();
Auto();
Auto();
Auto();
return 0;
}
3.1 概念
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化。
面试题:实现一个类,计算程序中创建出了多少个类对象。
class A
{
public:
A()
{
++_count1;
}
A(const A& aa)
{
++_count2;
}
//静态成员变量属于整个类
static int _count1;//声明
static int _count2;
int _a;
};
A Func(A a)
{
A copy(a);
return copy;
}
//定义
int A::_count1 = 0;
int A::_count2 = 0;
int main()
{
A a1;
A a2 = Func(a1);
cout << sizeof(a1) << endl;
cout << a1._count1 << endl;
cout << a2._count1 << endl;
cout << a1._count2 << endl;
cout << a2._count2 << endl;
cout << A::_count1 << endl;
cout << A::_count1 << endl;
return 0;
}
这里我们发现sizeof这个类,计算大小是不会计算静态成员的。因为静态成员为所有类对象所共享(在静态区),不属于某个具体的实例,所以不会纳入类内的计算。其次:在类内静态成员只是声明,静态成员变量必须在类外定义,定义时不添加static关键字如果静态成员是私有的就需要Get()函数提供接口来访问。
[ C++ ] 类与对象(下) 初始化列表,友元,static成员,内部类_小白又菜的博客-CSDN博客
3.2 特性
1). 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
因为静态数据成员在全局数据区分配内存,由本类的所有对象共享,所以,它不属于特定的类对象,不占用对象的内存,而是在所有对象之外开辟内存,在没有产生类对象时其作用域就可见。因此,在没有类的实例存在时,静态成员变量就已经存在,我们就可以操作它;
2). 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
静态成员变量存储在全局数据区。static成员变量的内存空间既不是在声明类时分配,也不是在创建对象时分配,而是在初始化时分配。静态成员变量必须初始化,而且只能在类体外进行。否则,编译能通过,链接不能通过。
3). 类静态成员可用 类名::静态成员 或者 对象.静态成员 来访问
静态成员变量是该类的所有对象所共有的。对于普通成员变量,每个类对象都有自己的一份拷贝。而静态成员变量一共就一份,无论这个类的对象被定义了多少个,静态成员变量只分配一次内存,由该类的所有对象共享访问。所以,静态数据成员的值对每个对象都是一样的,它的值可以更新;
4). 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
普通的成员函数一般都隐含了一个this指针,this指针指向类的对象本身,因为普通成员函数总是具体地属于类的某个具体对象的。当函数被调用时,系统会把当前对象的起始地址赋给 this 指针。通常情况下,this是缺省(默认)的。如函数fn()实际上是this->fn()。
与普通函数相比,静态成员函数属于类本身,而不作用于对象,因此它不具有this指针。正因为它没有指向某一个对象,所以它无法访问属于类对象的非静态成员变量和非静态成员函数,它只能调用其余的静态成员函数和静态成员变量。从另一个角度来看,由于静态成员函数和静态成员变量在类实例化之前就已经存在可以访问,而此时非静态成员还是不存在的,因此静态成员不能访问非静态成员。
笔试题:
下面程序段包含4个函数,其中具有隐含this指针的是( )
int f1();
class T
{
public:static int f2();
private:friend int f3();
protect:int f4();
};
A.f1
B.f2
C.f3
D.f4
A.全局函数不具备this指针
B.static函数不具备this指针
C.友元函数不具备this指针
D.正确,普通成员方法具有隐藏的this指针
5). 静态成员也是类的成员,受public、protected、private 访问限定符的限制
对应笔试题:
关于C++类中static 成员和对象成员的说法正确的是( )
A.static 成员变量在对象构造时生成
B.static 成员函数在对象成员函数中无法调用
C.static 成员函数没有this指针
D.static 成员函数不能访问static 成员变量
A.static成员变量在对象生成之前生成
B.普通成员函数是可以调用static函数的
C.static函数属于所有对象共享,不具备this指针
D.static函数唯一能够访问的就是static变量或者其他static函数
注意:static修饰的全局变量是声明与定义同时进行,就是说只要你在头文件中使用static声明一个全局变量,同时它也就被定义了。
static.h
#pragma once
#include<iostream>
using namespace std;
static int a = 3;
void fun();
static.cpp
void fun()
{
a = 1;
std::cout << "fun() -> a : " << a << std::endl;
}
test.cpp
#include"static.h"
int main()
{
fun();
cout << "在主函数中调用了一次fun()后 a :" << a << endl;
fun();
a = 10;
cout << "在主函数中给a赋值后 a :" << a << endl;
fun();
}
可以发现在test.cpp主函数中即使调用了fun(),而且fun()中也确实修改了a的值,因为在static.h中全局变量a被定义为3,但在主函数中输出a的值仍然为初值3,说明static修饰的全局变量在某一个cpp文件中的修改并不影响其他cpp文件对该变量的使用,同样在test.cpp中对于a的赋值可以说明虽然在static.cpp文件中定义了static类型的全局变量,但是该变量的值是可以改变的。
此外,如果上代码不加static修饰a就会报错
定义全局变量时使用static,意味着该变量的作用域只限于定义它的源文件中,其它源文件不能访问。
既然这种定义方式出现在头文件中,那么可以很自然地推测:包含了该头文件的所有源文件中都定义了这些变量,即该头文件被包含了多少次,这些变量就定义了多少次。
static去掉,编译的时候就会出现变量重定义的错误,这进一步证实了上面的推测,因为没有static的话变量的作用域是全局的,定义了两个以上的同名变量就会出现该错误。
此外我们可以对于static变量不初始化,编译器会自动默认初始化为0
所以下笔试题就迎刃而解:
在一个cpp文件里面,定义了一个static类型的全局变量,下面一个正确的描述是:( )
A.只能在该cpp所在的编译模块中使用该变量
B.该变量的值是不可改变的
C.该变量不能在类的成员函数中引用
D.这种变量只能是基本类型(如int,char)不能是C++类型
A.正确,static限制了变量具有文件域
B.static变量是可以被改变的
C.可以被正常访问使用,以及通过成员来进行引用
D.静态变量也可以是自定义类型的变量
3.3 求1+2+3+...+n
求1+2+3+...+n_牛客题霸_牛客网 (nowcoder.com)
描述
求1+2+3+...+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。
1) Sum a[n];调用一个对象会调用一次构造函数,调用n次构造函数,那么就可以实现走n次,再相加就得到结果,
得到如下代码:
#include<iostream>
using namespace std;
class Sum
{
public:
Sum(int ret = 0,int i = 1)
:_ret(ret)
,_i(i)
{
_ret += _i;
_i++;
}
int Get_ret()
{
return _ret;
}
private:
int _ret;//声明,在类里面
int _i;
};
class Solution {
public:
int Sum_Solution()
{
Sum a[10];
Sum x;
return x.Get_ret();
}
};
int main()
{
Solution Sum;
int ret = Sum.Sum_Solution();
cout << ret << endl;
return 0;
}
发现这样每次调用构造函数,就会进行初始化。 明显不对。
而 int Get_ret()
{
return _ret;
}是常见的访问私有变量的方法。
对上述代码进行修改:
2)static: 静态局部变量的特殊之处在于,函数被调用N次,初始化语句只会执行一次,之后则会取在内存静态区中储存的上次函数调用结束时该静态变量的值,而不是初始化语句。
class Sum
{
public:
Sum()//构造函数无参的自己写的
{
_ret += _i;
_i++;
}
//_ret是私有的,无法访问,故加上Get_ret函数,去访问成员函数
//int Get_ret()
//但是如果直接调用成员函数也调用不到
//一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间,
//存储类成员变量。所以如果不加上static就只能先实例化出对象
//return a[0].Get_ret();
static int Get_ret()
//类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
{
return _ret;
}
private:
static int _ret;//声明,在类里面
static int _i;
};
int Sum::_ret = 0;//在类外面定义,初始化
int Sum::_i = 1;
class Solution {
public:
int Sum_Solution(int n) {
Sum a[n];//调用一个对象会调用一次构造函数,调用n次构造函数
//那么就可以实现走n次
return Sum::Get_ret();
//静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
//return a[0].Get_ret();
}
};
四、友元
4.1 概念
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。
说明:
友元函数可访问类的私有和保护成员,但不是类的成员函数
友元函数不能用const修饰
友元函数可以在类定义的任何地方声明,不受类访问限定符限制
一个函数可以是多个类的友元函数
友元函数的调用与普通函数的调用原理相同
笔试题:
下面有关友元函数与成员函数的区别,描述错误的是?( )
A.友元函数不是类的成员函数,和普通全局函数的调用没有区别
B.友元函数和类的成员函数都可以访问类的私有成员变量或者是成员函数
C.类的成员函数是属于类的,调用的时候是通过指针this调用的
D.友元函数是有关键字friend修饰,调用的时候也是通过指针this调用的
A.友元函数不是类的成员函数,就相当于你的朋友再亲密也不是你的家人,既然不是类成员函数,那和普通成员函数调用一样,不需要通过对象调用
B.友元的目的就是为了访问类的私有数据,成员函数可以直接访问类的私有数据
C.类的成员函数属于类,调用时其内部数据会通过this指针来调用
D.友元函数不具备this指针,更谈不上通过this调用,故错误
注意:友元关系单向且不传递
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
友元关系是单向的,不具有交换性。
比如下述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time
类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
友元关系不能传递
如果B是A的友元,C是B的友元,则不能说明C时A的友元。
比如下述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
friend class Date;
声明日期类为时间类的友元类,则在日期类中就直接访问Time类中的私有成员变量
就是说A宣布B是我的朋友,A就认为B是朋友了,但是B没宣布A是它的朋友,所以B可以去A家里玩,但是A不能去B家里玩,除非B宣布A也是它的朋友才行。
class Time
{
friend class Date; // 声明日期类为时间类的友元类,则在日期类中就直接访问Time类中的私有成
员变量
public:
Time(int hour = 0, int minute = 0, int second = 0)
: _hour(hour)
, _minute(minute)
, _second(second)
{}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
void SetTimeOfDate(int hour, int minute, int second)
{
// 直接访问时间类私有的成员变量
_t._hour = hour;
_t._minute = minute;
_t._second = second;
}
private:
int _year;
int _month;
int _day;
Time _t;
};
4.2 内部类
概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
注意:内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
特性:
1). 内部类可以定义在外部类的public、protected、private都是可以的。
2). 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
3). sizeof(外部类)=外部类,和内部类没有任何关系。
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
}
};
};
int A::k = 1;
int main()
{
A::B b;
b.foo(A());
return 0;
}
4.3 计算日期到天数转换(笔试题)
计算日期到天数转换_牛客题霸_牛客网 (nowcoder.com)
#include <iostream>
using namespace std;
int main() {
int daysArray[13] = {0,31,59,90,120,151,181,212,243,273,304,334,365};
int year,month,day;
cin>>year>>month>>day;
int n = daysArray[month - 1] + day;
if((month>2)
&&((year%4 == 0 && year%100 != 0)
||(year %400 ==0)))
{
n += 1;
}
cout << n <<endl;
return 0;
}
4.4 日期差值
#include <iostream>
using namespace std;
// 十二个月份天数的和
int monthArray[13] = {0,31,59,90,120,151,181,212,243,273,304,334,365};
int isLeapYear(int year)
{
if ((year % 400 == 0) || (year % 4 == 0 && year % 100 != 0))
{
return 1;
}
return 0;
}
int AddDate(int year, int month, int day)
{
int ret = 0;
ret = (year - 1) * 365 + monthArray[month - 1] + day;
if (month > 2 && isLeapYear(year) == 1)
{
ret += 1;
}
int i = 0;
for(i = 1;i <= (year-1);i++)
{
if(isLeapYear(i) == 1)
{
ret += 1;
}
}
return ret;
}
int main()
{
int year1, month1, day1;
scanf("%4d%2d%2d", &year1, &month1, &day1);
int year2, month2, day2;
scanf("%4d%2d%2d", &year2, &month2, &day2);
int n1 = AddDate(year1, month1, day1);
int n2 = AddDate(year2, month2, day2);
cout << abs(n1-n2) + 1 << endl;
}
4.5 打印日期
%d是整型输出格式。02的意思是如果输出的整型数不足两位,左侧用0补齐。
1、%d就是普通的输出了
2、% 2d是将数字按宽度为2,采用右对齐方式输出,若数据位数不到2位,则左边补空格。
3、% 02d,和% 2d差不多,只不过左边补0
4、%.2d从执行效果来看,和% 02d一样
#include <iostream>
int monthArray[13] = {0,31,59,90,120,151,181,212,243,273,304,334,365};
int monthArray_LeapY[13] = {0,31,60,91,121,152,182,213,244,274,305,335,366};
bool isLeapYear(int year)
{
if( (year % 400 == 0) || (year%4 == 0 && year%100 != 0) )
{
return true;
}
return false;
}
int main()
{
int y, n;
scanf("%4d%3d", &y, &n);
int day = 0;
int month = 0;
int i = 0;
if(isLeapYear(y) == 0)
{
for(i = 1;i<=12;i++)
{
if(n / (monthArray[i] + 1) == 0)
{
month = i;
break;
}
}
day = n - monthArray[month - 1];
}
else
{
for(i = 1;i<=12;i++)
{
if(n / (monthArray_LeapY[i] + 1) == 0)
{
month = i;
break;
}
}
day = n - monthArray_LeapY[month - 1];
}
printf("%4d-%02d-%02d\n",y ,month ,day );
}