C++复习day05

2024-09-07 03:36
文章标签 c++ 复习 day05

本文主要是介绍C++复习day05,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

类和对象

1. 面向对象和面向过程的区别是什么?(开放性问题)
1. **抽象级别**:- **面向对象**:以对象(数据和方法的集合)为中心,强调的是数据和行为的封装。- **面向过程**:以过程(函数或子程序)为中心,强调的是步骤和顺序。2. **数据和方法的关系**:- **面向对象**:数据和处理数据的方法封装在对象中,对象可以包含数据和操作数据的方法。- **面向过程**:数据和处理数据的方法是分离的,通常数据结构和处理这些数据的函数是分开的。3. **模块化**:- **面向对象**:通过类和对象来实现模块化,类定义了对象的蓝图。- **面向过程**:通过函数和过程来实现模块化,函数是独立的代码块。4. **代码重用**:- **面向对象**:通过继承和多态性,可以更容易地重用代码。- **面向过程**:代码重用通常通过函数库来实现,但可能不如面向对象那样灵活。5. **维护和扩展**:- **面向对象**:由于封装和模块化,通常更容易维护和扩展。- **面向过程**:随着系统的增长,维护和扩展可能会变得更加困难。6. **设计复杂性**:- **面向对象**:设计可能更复杂,因为需要考虑类之间的关系和继承结构。- **面向过程**:设计可能更直接,因为关注点在于函数的调用和执行。7. **语言支持**:- **面向对象**:许多现代编程语言(如Java、C++、Python、Ruby)天然支持面向对象编程。- **面向过程**:几乎所有编程语言都支持过程化编程,但一些语言(如C)在支持面向对象特性方面可能不如其他语言。8. **性能**:- **面向对象**:可能会有额外的开销,因为需要处理对象的创建和方法调用。- **面向过程**:通常在性能上更高效,因为直接调用函数通常比创建对象和调用方法更快。
2.类大小的计算

这里同样是和结构体的内存对齐做法相同,需要注意的是空类的大小是1(主要是为了在地址空间中占位,表示存在这个类)

3.class和struct的区别
  1. struct 一般用于描述一个数据结构集合,而 class 是对一个对象数据的封装;
  2. struct 中默认的访问控制权限是 public 的,而 class 中默认的访问控制权限是 private 的,例如:
struct A{
int iNum; // 默认访问控制权限是 public
}
class B{
int iNum; // 默认访问控制权限是 private
}
  1. 在继承关系中,struct 默认是公有继承,而 class 是私有继承;
  2. class 关键字可以用于定义模板参数,就像 typename,而 struct 不能用于定义模板参数,例如:
template<typename T, typename Y> // 可以把typename 换成 class
int Func(const T& t, const Y& y) {
//TODO
}
3.this指针
1)this指针存放在哪里?

this指针是在栈上的,因为他是一个形参,当然有时候在会存在于寄存器上。因为有时候需要频繁的使用this指针,所以放到了寄存器,便于更加快速的使用this指针

2)this指针可以为空嘛?

this指针通常不能为nullptr,因为他是当前对象的地址,然而有一些特殊的情况

1. 静态成员函数:在静态成员函数中,this指针是不可用的,因为静态成员函数不依赖于任何特定的实例。
2 .如果是去调用成员函数,由于成员函数并不在对象中,this不会进行解引用,所以即使this是空指针,也不会崩溃。
3. 如果this指针是空,还访问了成员变量,那么成员就会崩溃,原因是对空指针进行了解引用。

4.八个默认成员函数

首先先整体清点一下是哪八个成员函数
构造函数,析构函数,拷贝构造函数,赋值运算符重载,取地址操作符重载,const成员函数,移动构造,移动赋值

1)构造函数
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。虽然名称叫构造,但是构造函数的主要任
务并不是开空间创建对象,而是初始化对象

特性:

1. 函数名与类名相同。
2. 无返回值。
3. 对象实例化时编译器自动调用对应的构造函数。
4. 构造函数可以重载。

问: 关于编译器生成的默认成员函数,很多童鞋会有疑惑:不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?d对象调用了编译器生成的默认构造函数,但是d对象_year/_month/_day,依旧是随机值。也就说在这里编译器生成的
默认构造函数并没有什么用??
答: C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如int/char…,自定义类型就是我们使用class/struct/union等自己定义的类型,看看下面的程序,就会发现编译器生成默认的构造函数会对自定类型成员_t调用的它的默认成员函数。

class Time
{
public:Time(){cout << "Time()" << endl;_hour = 0;_minute = 0;_second = 0;}
private:int _hour;int _minute;int _second;
};
class Date
{
private:// 基本类型(内置类型)int _year;int _month;int _day;// 自定义类型Time _t;
};
int main()
{Date d;return 0;
}

注意:C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。

class Time
{
public:Time(){cout << "Time()" << endl;_hour = 0;_minute = 0;_second = 0;}
private:int _hour;int _minute;int _second;
};
class Date
{
private:// 基本类型(内置类型)int _year = 1970;int _month = 1;int _day = 1;// 自定义类型Time _t;
};
int main()
{Date d;return 0;
}
2) 析构函数
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由
编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。

析构函数是特殊的成员函数,其特征如下:

  1. 析构函数名是在类名前加上字符 ~。
  2. 无参数无返回值类型。
  3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构
    函数不能重载
  4. 对象生命周期结束时,C++编译系统系统自动调用析构函数
  5. 关于编译器自动生成的析构函数,是否会完成一些事情呢?下面的程序我们会看到,编译器生成的默认析构函数,对自定类型成员调用它的析构函数。
  6. 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。
3) 拷贝构造

拷贝构造函数也是特殊的成员函数,其特征如下

  1. 拷贝构造函数是构造函数的一个重载形式
  2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用
  3. 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
  4. . 编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显式实现吗?当然像日期类这样的类是没必要的。那么下面的类呢?验证一下试试?
// 这里会发现下面的程序会崩溃掉?这里就需要我们以后讲的深拷贝去解决。
typedef int DataType;
class Stack
{
public:Stack(size_t capacity = 10){_array = (DataType*)malloc(capacity * sizeof(DataType));if (nullptr == _array){perror("malloc申请空间失败");return;}size = 0;_capacity = capacity;}void Push(const DataType& data){// CheckCapacity();_array[_size] = data;_size++;}~Stack(){if (_array){free(_array);_array = nullptr;_capacity = 0;_size = 0;}}
private:DataType* _array;size_t _size;size_t _capacity;
};
int main()
{Stack s1;s1.Push(1);s1.Push(2);s1.Push(3);s1.Push(4);Stack s2(s1);return 0;
}

这种情况是会崩溃的,因为同一块空间被释放了两次

4)运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其
返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)

注意:

  • 不能通过连接其他符号来创建新的操作符:比如operator@
  • 重载操作符必须有一个类类型参数
  • 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
  • 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
  • .* :: sizeof ?: .这五类运算符是不能够被重载的,这个在笔试题中会经常性的出现
    比如我们重载一下Date类的==和<<,代码大概就像这样:
#include <iostream>using namespace std;class Date
{friend ostream& operator << (ostream& out, const Date& d);friend bool operator==(const Date& d1, const Date& d2);
public:Date(int year, int month, int day) :_year(year),_month(month),_day(day){ }~Date(){cout << "~Date()" << endl;}private:int _year;int _month;int _day;
};
bool operator==(const Date& d1, const Date& d2)
{return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day;
}
ostream& operator << (ostream & out, const Date& d)
{out << d._year << d._month << d._day;return out;
}
int main()
{Date d1(1, 1, 1),d2(2,2,2);cout << (d1 == d2) << endl;;cout << d1 << endl;return 0;
}

赋值运算符重载格式:

  • 参数类型:const T&,传递引用可以提高传参效率
  • 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
  • 检测是否自己给自己赋值
  • 返回*this :要复合连续赋值的含义
#include <iostream>using namespace std;class Date
{friend ostream& operator << (ostream& out, const Date& d);friend bool operator==(const Date& d1, const Date& d2);
public:Date(int year, int month, int day) :_year(year),_month(month),_day(day){ }Date(const Date& d){_year = d._year;_month = d._month;_day = d._day;}~Date(){cout << "~Date()" << endl;}const Date& operator=(const Date& d){_year = d._year;_month = d._month;_day = d._day;return *this;}
private:int _year;int _month;int _day;
};
bool operator==(const Date& d1, const Date& d2)
{return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day;
}
ostream& operator << (ostream & out, const Date& d)
{out << d._year << d._month << d._day;return out;
}
int main()
{Date d1(1, 1, 1),d2(2,2,2);cout << (d1 == d2) << endl;d1 = d2 = Date(3,3,3);cout << (d1 == d2) << endl;return 0;
}

注意:

赋值运算符只能重载成类的成员函数不能重载成全局函数
用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符
重载完成赋值。

这里提一嘴关于前置++和后置++的实现(–也是同理的)

#include <iostream>using namespace std;class Date
{friend ostream& operator << (ostream& out, const Date& d);friend bool operator==(const Date& d1, const Date& d2);
public:Date(int year, int month, int day) :_year(year),_month(month),_day(day){ }Date(const Date& d){_year = d._year;_month = d._month;_day = d._day;}Date& operator++()// 前置++{_day += 1; //这里为例简便,我就不考虑什么月份,天数什么的了}Date operator++(int) //后置++{Date temp(*this);_day += 1;return temp;}~Date(){cout << "~Date()" << endl;}const Date& operator=(const Date& d){_year = d._year;_month = d._month;_day = d._day;return *this;}
private:int _year;int _month;int _day;
};
bool operator==(const Date& d1, const Date& d2)
{return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day;
}
ostream& operator << (ostream & out, const Date& d)
{out << d._year << d._month << d._day;return out;
}
int main()
{Date d1(1, 1, 1),d2(2,2,2);cout << (d1 == d2) << endl;d1 = d2 = Date(3,3,3);cout << (d1 == d2) << endl;return 0;
}
5)const成员
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。

请思考下面的几个问题:

  1. const对象可以调用非const成员函数吗?
  2. 非const对象可以调用const成员函数吗?
  3. const成员函数内可以调用其它的非const成员函数吗?
  4. 非const成员函数内可以调用其它的const成员函数吗?
const对象不可以调用非const成员函数,非const对象可以调用const成员函数。
const成员函数不可以调用其他的非const成员函数,非const成员函数可以调用const成员函数
6)取地址操作符重载

这两个默认成员函数一般不用重新定义 ,编译器默认会生成。
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需
要重载,比如想让别人获取到指定的内容!

7/8) 移动构造和移动赋值

(这两个成员函数放到一起进行讨论)
首先来介绍一下什么叫做左值引用,什么叫做右值引用,以及他们的区别是什么

传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。
什么是左值?什么是左值引用?
左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。
#include <iostream>
using namespace std;int main()
{int*p = new int(0);int b = 1;const int c = 2;int*&rp = p;int&pb = b;const int&pc = c;int&pvalue = *p;return 0;
}
什么是右值?什么是右值引用?
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。右值引用就是对右值的引用,给右值取别名。
int main()
{
double x = 1.1, y = 2.2;
// 以下几个都是常见的右值
10;
x + y;
fmin(x, y);
// 以下几个都是对右值的右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
// 这里编译会报错:error C2106: “=”: 左操作数必须为左值
10 = 1;
x + y = 1;
fmin(x, y) = 1;
return 0;
}
需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址,也就是说例如:不能取字面量10的地址,但是rr1引用后,可以对rr1取地址,也可以修改rr1。如果不想rr1被修改,可以用const int&& rr1 去引用,是不是感觉很神奇,这个了解一下实际中右值引用的使用场景并不在于此,这个特性也不重要。
int main()
{double x = 1.1, y = 2.2;int&& rr1 = 10;const double&& rr2 = x + y;rr1 = 20;rr2 = 5.5;  // 报错return 0;
}

左值引用与右值引用比较
左值引用总结:

  1. 左值引用只能引用左值,不能引用右值。
  2. 但是const左值引用既可引用左值,也可引用右值。(临时常量具有常性)
#include <iostream>
using namespace std;int main()
{//int&a = 10;//这种是错误的const int&a = 10;//这种是正确的return 0;
}

右值引用总结:

  1. 右值引用只能右值,不能引用左值。
  2. 但是右值引用可以move以后的左值。

来看看右值引用使用场景和意义
前面我们可以看到左值引用既可以引用左值和又可以引用右值,那为什么C++11还要提出右值引
用呢?是不是化蛇添足呢?下面我们来看看左值引用的短板,右值引用是如何补齐这个短板的!

namespace bit
{class string{public:typedef char *iterator;iterator begin(){return _str;}iterator end(){return _str + _size;}string(const char *str = ""): _size(strlen(str)), _capacity(_size){// cout << "string(char* str)" << endl;_str = new char[_capacity + 1];strcpy(_str, str);}// s1.swap(s2)void swap(string &s){::swap(_str, s._str);::swap(_size, s._size);::swap(_capacity, s._capacity);}// 拷贝构造string(const string &s): _str(nullptr){cout << "string(const string& s) -- 深拷贝" << endl;string tmp(s._str);swap(tmp);}// 赋值重载string &operator=(const string &s){cout << "string& operator=(string s) -- 深拷贝" << endl;string tmp(s);swap(tmp);return *this;}// 移动构造string(string &&s): _str(nullptr), _size(0), _capacity(0){cout << "string(string&& s) -- 移动语义" << endl;swap(s);}// 移动赋值string &operator=(string &&s){cout << "string& operator=(string&& s) -- 移动语义" << endl;swap(s);return *this;}~string(){delete[] _str;_str = nullptr;}char &operator[](size_t pos){assert(pos < _size);return _str[pos];}void reserve(size_t n){if (n > _capacity){char *tmp = new char[n + 1];strcpy(tmp, _str);delete[] _str;_str = tmp;_capacity = n;}}void push_back(char ch){if (_size >= _capacity){size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;reserve(newcapacity);}_str[_size] = ch;++_size;_str[_size] = '\0';}// string operator+=(char ch)string &operator+=(char ch){push_back(ch);return *this;}const char *c_str() const{return _str;}private:char *_str;size_t _size;size_t _capacity; // 不包含最后做标识的\0};
}

不妨先看看左值引用的应用场景:

void func1(bit::string s)
{}
void func2(const bit::string& s)
{}
int main()
{bit::string s1("hello world");// func1和func2的调用我们可以看到左值引用做参数减少了拷贝,提高效率的使用场景和价值func1(s1);func2(s1);// string operator+=(char ch) 传值返回存在深拷贝// string& operator+=(char ch) 传左值引用没有拷贝提高了效率s1 += '!';return 0;
}

问:左值引用的短板是什么?

但是当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,只能传值返回。例如:bit::string to_string(int value)函数中可以看到,这里只能使用传值返回,传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷贝构造)。

也就是说,只要是局部的对象需要返回,左值引用就是失去了作用,因为出作用域之后局部对象就会被销毁,当时候指向的就是一片位置的区域。
那么如果解决这个问题呢?

右值引用和移动语义解决上述问题:
在bit::string中增加移动构造,移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不
用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己(偷梁换柱)。

来看看怎么实现

// 移动构造
string(string&& s):_str(nullptr),_size(0),_capacity(0)
{cout << "string(string&& s) -- 移动语义" << endl;swap(s);
}
不仅仅有移动构造,还有移动赋值:
在bit::string类中增加移动赋值函数,再去调用bit::to_string(1234),不过这次是将
bit::to_string(1234)返回的右值对象赋值给ret1对象,这时调用的是移动构造。
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动语义" << endl;
swap(s);
return *this;
}
int main()
{bit::string ret1;ret1 = bit::to_string(1234);return 0;
}
// 运行结果:
// string(string&& s) -- 移动语义
// string& operator=(string&& s) -- 移动语义

这篇关于C++复习day05的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1143942

相关文章

【C++ Primer Plus习题】13.4

大家好,这里是国中之林! ❥前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。有兴趣的可以点点进去看看← 问题: 解答: main.cpp #include <iostream>#include "port.h"int main() {Port p1;Port p2("Abc", "Bcc", 30);std::cout <<

C++包装器

包装器 在 C++ 中,“包装器”通常指的是一种设计模式或编程技巧,用于封装其他代码或对象,使其更易于使用、管理或扩展。包装器的概念在编程中非常普遍,可以用于函数、类、库等多个方面。下面是几个常见的 “包装器” 类型: 1. 函数包装器 函数包装器用于封装一个或多个函数,使其接口更统一或更便于调用。例如,std::function 是一个通用的函数包装器,它可以存储任意可调用对象(函数、函数

C++11第三弹:lambda表达式 | 新的类功能 | 模板的可变参数

🌈个人主页: 南桥几晴秋 🌈C++专栏: 南桥谈C++ 🌈C语言专栏: C语言学习系列 🌈Linux学习专栏: 南桥谈Linux 🌈数据结构学习专栏: 数据结构杂谈 🌈数据库学习专栏: 南桥谈MySQL 🌈Qt学习专栏: 南桥谈Qt 🌈菜鸡代码练习: 练习随想记录 🌈git学习: 南桥谈Git 🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈�

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

06 C++Lambda表达式

lambda表达式的定义 没有显式模版形参的lambda表达式 [捕获] 前属性 (形参列表) 说明符 异常 后属性 尾随类型 约束 {函数体} 有显式模版形参的lambda表达式 [捕获] <模版形参> 模版约束 前属性 (形参列表) 说明符 异常 后属性 尾随类型 约束 {函数体} 含义 捕获:包含零个或者多个捕获符的逗号分隔列表 模板形参:用于泛型lambda提供个模板形参的名

6.1.数据结构-c/c++堆详解下篇(堆排序,TopK问题)

上篇:6.1.数据结构-c/c++模拟实现堆上篇(向下,上调整算法,建堆,增删数据)-CSDN博客 本章重点 1.使用堆来完成堆排序 2.使用堆解决TopK问题 目录 一.堆排序 1.1 思路 1.2 代码 1.3 简单测试 二.TopK问题 2.1 思路(求最小): 2.2 C语言代码(手写堆) 2.3 C++代码(使用优先级队列 priority_queue)

【C++高阶】C++类型转换全攻略:深入理解并高效应用

📝个人主页🌹:Eternity._ ⏩收录专栏⏪:C++ “ 登神长阶 ” 🤡往期回顾🤡:C++ 智能指针 🌹🌹期待您的关注 🌹🌹 ❀C++的类型转换 📒1. C语言中的类型转换📚2. C++强制类型转换⛰️static_cast🌞reinterpret_cast⭐const_cast🍁dynamic_cast 📜3. C++强制类型转换的原因📝

C++——stack、queue的实现及deque的介绍

目录 1.stack与queue的实现 1.1stack的实现  1.2 queue的实现 2.重温vector、list、stack、queue的介绍 2.1 STL标准库中stack和queue的底层结构  3.deque的简单介绍 3.1为什么选择deque作为stack和queue的底层默认容器  3.2 STL中对stack与queue的模拟实现 ①stack模拟实现

c++的初始化列表与const成员

初始化列表与const成员 const成员 使用const修饰的类、结构、联合的成员变量,在类对象创建完成前一定要初始化。 不能在构造函数中初始化const成员,因为执行构造函数时,类对象已经创建完成,只有类对象创建完成才能调用成员函数,构造函数虽然特殊但也是成员函数。 在定义const成员时进行初始化,该语法只有在C11语法标准下才支持。 初始化列表 在构造函数小括号后面,主要用于给

2024/9/8 c++ smart

1.通过自己编写的class来实现unique_ptr指针的功能 #include <iostream> using namespace std; template<class T> class unique_ptr { public:         //无参构造函数         unique_ptr();         //有参构造函数         unique_ptr(