首页 >> 大全

C++——类与对象补充

2023-12-21 大全 28 作者:考证青年

一,隐式类型转换和关键字 1.1 隐式类型转换

先定义一个类,这个类后面会经常用到

class A
{
public:A(){cout << "A()" << endl;}A(int a):_a1(a){cout << "A(int a)" << endl;}A(int a1,int a2):_a1(a1), _a2(_a2){cout << "A(int a1,int a2)" << endl;}A(const A& aa):_a1(aa._a1){cout << "A(const A& aa)" << endl;}~A(){cout << "~A()" << endl;}
private:int _a1;int _a2;
};

可以发现下面的代码都可以通过编译

void main1()
{//单参数构造函数 C++98A aa1(1);A aa2 = 1; //多参数构造函数 C++11A aa3(1, 1);A aa4 = { 2,2 };
}

可以看到,数字1是int类型,但是可以用来初始化甚至可以赋值给A类型,从曾经C语言的角度来看上面的代码是行不通的,但这是C++,C++对这种情况进行了处理,首先会生成一个临时的具有常性的对象,然后再把对象赋值给自定义类型,下面的两条语句可以证明该对象存在

const A& ref1 = 10; //可以看出有临时对象生成,并且具有常性
const A& ref2 = {2,2};

在最新的VS编译器下,这个临时对象被优化掉了,就没有临时对象生成和拷贝了,运行代码时可以发现没有打印拷贝构造的内容,六个对象只调用了构造和析构函数

1.2 关键字

要想禁止构造发生隐式类型转换,我们可以在构造函数前面加上关键字


//explict关键字,加在构造函数前面就可以禁止隐式类型转换
explicit A(int a):_a1(a)
{cout << "A(int a)" << endl;
}
explicit A(int a1,int a2):_a1(a1), _a2(_a2)
{cout << "A(int a1,int a2)" << endl;
}
A(const A& aa) //产生临时对象:_a1(aa._a1)
{cout << "A(const A& aa)" << endl;
}

这样就能隐式类型转换了,可以看到将会发生隐式类型转换的语句无法通过编译

所以我们可以得出一个结论:构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用

二,成员

声明为的类成员称为类的静态成员,用修饰的成员变量,称之为静态成员变量;用修饰的成员函数,称之为静态成员函数,静态成员变量一定要在类外面进行处初始化

关于成员不好理解,下面我们通过一个题目并结合注释来学习

//计算1到n之间的值的和,不允许使用循环判断以及位运算
class Sum
{
public:Sum(){_sum += _i;++_i;}~Sum(){cout << "~Sum()" << endl;}static int _i;static int _sum; //定义为静态成员变量
};
class Solution
{public:int Sum_Solution(int n){//Sum a[n]; //VS不支持变长数组Sum* ptr = new Sum[n];return Sum::_sum;}
};
int Sum::_i = 1;
int Sum::_sum = 0; //Sum里的静态成员变量在类外进行初始化
void main3()
{Solution s;cout << s.Sum_Solution(100) << endl;//我们要求1到100之间的数的和,由于Sum中记录数据的_sum为静态成员变量,所以在单个函数栈帧中只会初始化一次//题目要求不能使用任何的while for if else以及位运算,所以我们通过在构造函数里对_sum进行相加操作来解决问题,//因为定义一次对象就要调用一次构造函数,所以我们定义100个对象就会调用100次构造函数,相当于间接实现了循环相加的功能
}

关于静态成员的特性如下几点:

①静态成员为所有类对象共享,存在静态区

②静态成员变量必须在类外面定义,并且不加关键字,类中只是声明

③类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问

④静态成员函数没有隐藏的this指针,不能访问任何非静态成员

⑤静态成员也是类的成员,野兽访问限定符的限制

三,匿名对象

可以发现,匿名对象就是类名加上一个括号,不用取名字,而且匿名对象的声明周期只有这一行,调用构造函数后直接调用析构函数

然后就是关于匿名对象的用途,比如我们可以优化上面求1到100的和的main3()函数

void main3()
{Solution s;cout << s.Sum_Solution(100) << endl;//要定义一个对象//Solution s1();  不能这样定义对象,因为分不清这是定义对象还是函数声明//匿名对象有什么用?//比如前面我们求和,我们还要定义一个对象来求,一般不这样,一般用下面这样的方法cout << Solution().Sum_Solution(100) << endl;
}

匿名对象也可以用来传返回值,所以我们还可以对上面的main3再改造

A func(int n)
{int ret = Solution().Sum_Solution(n);//直接构建一个匿名对象返回return A(ret);
}
void main3()
{func(100);
}

当然还要其他的一些使用场景,我们以后遇见了再说

四,拷贝对象时编译器的一些优化

class A
{
public:A(int a):_a(a){cout << "A(int a)" << endl;}A(const A& aa) :_a1(aa._a){cout << "A(const A& aa)" << endl;}A& operator=(const A& aa){cout << "A& operator=(const A& aa)" << endl;if (this != &aa){_a1 = aa._a1;}return *this;}~A() {cout << "~A()" << endl; }
private:int _a;
};

观察下列代码以及运行结果

//拷贝对象时编译器的一些优化
void func1(A aa){}
void func2(const A& aa){}void main4()
{A aa = 1;   //构造+拷贝构造 -> 优化为直接构造func1(aa);  //仅有一次拷贝构造func1(2);   //构造+拷贝构造 -> 优化为直接构造func1(A(3));//构造+拷贝构造 -> 优化为直接构造cout << "------------" << endl;func2(aa);  //引用传参,什么也不打印func2(2);   //引用传参,无优化也不需要优化func2(A(3));//引用传参,无优化也不需要优化
}

可以发现,对象传参的时候本来有很多构造和拷贝的现在已经全部优化成了只拷贝一次,而且对比引用传参,可以发现引用引用传参传对象的时候会少生成一个对象,所以以后我们使用函数传参的时候尽量选择使用引用传参

如下代码

A func3()
{A aa;return aa;
}
A func4()
{return A();
}
void main5()
{func3();         //构造+拷贝构造 -> 优化为直接构造A aa1 = func3(); //构造+拷贝构造 -> 一个构造+析构cout << "--------------------" << endl;A aa2;aa2 = func3();//一次构造,一次赋值重载cout << "------------------" << endl;func4();         //构造+拷贝构造 -> 优化为直接构造A aa3 = func4(); //构造+拷贝构造 -> 一个构造
}

最后两次析构分别是aa1和aa2对象的析构

对象返回总结两个点:

①接收返回值对象尽量拷贝构造方式接收,不要赋值接收,上面aa2就是赋值接收,aa1就是和aa3就是拷贝构造接收

②函数中返回对象时,尽量返回匿名对象,注意不是必须

五,对于类和对象一些问题的解答 ①默认生成的构造和析构函数干了什么呢?

我们不写构造和析构,编译器会默认生成,在构造函数中,对于内置类型不做处理,是随机值,对于自定义类型调用它自己的构造和析构函数

②默认生成的拷贝构造和赋值重载干了什么呢?

这俩函数的用途是一样的,对于内置类型完成浅拷贝/值拷贝,按字节一个一个拷贝,对于自定义类型,一样的,去调用这个类型的拷贝构造和赋值重载,运算符重在的意义是增强程序可读性

③什么情况下需要我们自己实现拷贝构造?

简单来说,就是如果我们使用了new等开辟动态内存的函数,并且需要为此自己实现析构函数释放空间的时候,就需要实现拷贝构造,深拷贝就是一个典型的例子,关于深拷贝在STL中的博客中有详细说明

(重载操作里必须有一个类类型参数,内置类型的运算符比如+,不能改变其含义,. * :: 以及三目运算符?:不能重载)

④为什么构造函数有初始化列表,有什么用?

先看下面的现象

初始化列表是所有成员变量定义的地方,不论成员变量是否显示在初始化列表写,编译器的每个变量都会通过初始化列表定义初始化

const变量只有一次初始化的机会,必须在定义的位置初始化,但是类中的成员变量只能定义,所以类中有const变量时会报错,上面的图,因为编译器对内置类型不做处理,所以必须处理const变量,必须给每个成员找一个定义的位置,所以就有了初始化列表

⑤哪些成员必须在初始化列表初始化?

const成员,引用,没有默认构造的自定义类型成员

⑥为什么实现=()的时候以对象引用做返回值?

先看一个现象,上面的A类型中的=()我们是用A&做返回值,那么我们改成void发现程序照样能跑

void operator=(const A& aa)
{cout << "void operator=(const A& aa)" << endl;if (this != &aa){_a1 = aa._a1;}
}

其实以对象引用做返回值是为了支持连续赋值,如以下代码,没有以对象引用做返回值时会报错

所以,为了支持连续赋值,我们选择使用对象引用做返回值,并在 *this

⑦类成员函数后面加上const是什么意思?

上面的初始化列表问题我们可以感受到,我们不喜欢在类中定义const,会造成很多不必要的麻烦,但是在C++中,我们经常使用在函数传参的时候传const &,比图void Func(const A& x)。

但也有不含参数的函数比如void Print(),打印时通过this来访问成员变量的,但是这里的this并没有显示出来也就无法在括号里加上const关键字,所以我们通过在括号后面加上const来修饰this,使this的成员变量不可修改

关于我们

最火推荐

小编推荐

联系我们


版权声明:本站内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 88@qq.com 举报,一经查实,本站将立刻删除。备案号:桂ICP备2021009421号
Powered By Z-BlogPHP.
复制成功
微信号:
我知道了