c++编码规范与知识笔记(二)

首先补充一下上一节的拷贝构造函数使用场景:
更详细的可参见此博主的文章(很不错)

在下面几种情况下会调用拷贝构造函数
a、 显式或隐式地用同类型的一个对象来初始化另外一个对象。如上例中,用对象c初始化d;
b、 作为实参(argument)传递给一个函数。如CClass(const CClass c_class)中,就会调用CClass的拷贝构造函数;
c、 在函数体内返回一个对象时,也会调用返回值类型的拷贝构造函数;
d、 初始化序列容器中的元素时。比如 vector< string > svec(5),string的缺省构造函数和拷贝构造函数都会被调用;
e、 用列表的方式初始化数组元素时。string a[] = {string(“hello”), string(“world”)}; 会调用string的拷贝构造函数。
如果在没有显式声明构造函数的情况下,编译器都会为一个类合成一个缺省的构造函数。如果在一个类中声明了一个构造函数,那么就会阻止编译器为该类合成缺省的构造函数。和构造函数不同的是,即便定义了其他构造函数(但没有定义拷贝构造函数),编译器总是会为我们合成一个拷贝构造函数。
另外函数的返回值是不是引用也有很大的区别,返回的不是引用的时候,只是一个简单的对象,此时需要调用拷贝构造函数,否则,如果是引用的话就不需要调用拷贝构造函数。

关于为什么拷贝构造函数参数必须使用引用——把形参拷贝到实参会调用复制构造函数。因此如果允许复制构造函数传值,那么会形成永无休止的递归并造成栈溢出。因此C++的标准不允许复制构造函数传值参数,而必须是传引用或者常量引用。

当返回对象时,C++编译器将调用类默认的拷贝构造函数,将对象赋值给一个匿名对象并将其扔出去
这个类似于拷贝构造函数的第四种方法,此时如果外面没有同一个类型的对象将其接住,就会直接析构掉。
若定义的是返回引用的函数,此处会直接返回对象本身不会再创建一个匿名对象

并且:
返回对象会涉及到生成返回对象的副本。因此,返回对象的时间成本包括了调用复制构造函数来生成副本所需的时间和调用析构函数删除副本所需的时间。返回引用可以节省时间和内存。直接返回对象与按值传递对象类似,他们都生成临时副本。同样,返回引用与按引用传递对象类似,调用和被调用的函数对同一个对象进行操作
并不是总是可以返回引用的。比如函数不能返回在函数中创建的临时对象的引用。因为当函数结束调用时,临时对象将消失,因此这种引用是非法的。在这种情况下,应返回对象,以生成一个调用程序可以使用的副本。

利用操作符重载的例子加以说明

#include "iostream"
using namespace std;
 
class Complex
{
public:
friend ostream& operator<<(ostream &cout, Complex & c1);
//friend ostream& operator<<(ostream &cout, Complex c1);
Complex(int a, int b)
{
this->a = a;
this->b = b;
}
Complex(const Complex &obj)
{
cout << "拷贝构造函数" << endl;
}
private:
int a;
int b;
};
 
ostream& operator<<(ostream &cout, Complex &c1)//这儿要写成c1的引用,不然改的内容就不会保存在实参c1里
//ostream operator<<(ostream &cout, Complex &c1)
//声明这个返回对象的函数时,编译不通过,因为返回的匿名对象在链式编程时直接就析构了,不能通过链式编程
{
cout << c1.a <<"+"<< c1.b << "i" << endl;
return cout;
}
void main()
{
Complex c1(1,2),c2(4,5);
cout << c1 << c2 << endl;//此处,若是声明返回引用的函数,则直接返回对象本身,不执行拷贝构造函数
若定义的是返回对象的函数,此处会直接返回对象本身不会再创建一个匿名对象
system("pause");
 
}

============================================================================

下面开始本次的内容:

1. C++11引入final特殊标识符,可以使得类不能被继承
class B final {}; 

2.  C++11:派生类不继承的特殊函数
(1)     析构函数
(2)     友元函数

继承基类构造函数
(1)     using A::A;  继承所有基类ctor
(2)     不能仅继承指定的某个基类ctor


调用继承的构造函数:
struct A { // 等价于 class A { public:
    A(int i) {}
    A(double d, int i) {}
// ...
};
struct B : A {  // C++11
    using A::A; // 继承基类所有构造函数
    int d{0};   // 就地初始化
};
int main() {
    B b(1);   // 调A(int i)
}


当程序员在派生类构造函数中显式调用基类构造函数时,应将被调用基类构造函数放在:
派生类构造函数体中


3.A class should be declared in a header file and defined in a source file where the name of the files match the name of the class.

类应该在头文件中声明并在源文件中定义,俩文件名字应该与类名相同
例如:MyClass.h, MyClass.cpp
"例外的是,模板类的声明和定义都要放在头文件中"


4. Class variables should never be declared public.

类成员变量不可被声明为public
说明:公有变量违背了C++的信息隐藏原则。例外的是,如果class只是一个数据结构,类似C语言中的struct,则可将类变量声明为公有


5. 派生类构造函数必须调用基类构造函数。


6. Override与final的应用;
(1) override显式声明覆写
C++11引入override标识符,指定一个虚函数覆写另一个虚函数。

class A {
public:
  virtual void foo() {}
  void bar() {}
};
class B : public A {
public:
  void foo() const override { // 错误: B::foo 不覆写 A::foo
  }                           // (签名不匹配)
  void foo() override;   // OK : B::foo 覆写 A::foo
  void bar() override {} // 错误: A::bar 非虚
};
void B::foo() override {// 错误: override只能放到类内使用
}
==== Note ====
override的价值在于:避免程序员在覆写时错命名或无虚函数导致隐藏bug
https://zh.cppreference.com/w/cpp/language/override

 

(2) final 显式声明禁止覆写
C++11引入final特殊标识符,指定派生类不能覆写虚函数
struct Base {
    virtual void foo();
};
struct A : Base 
{ 
    void foo() final; // A::foo 被覆写且是最终覆写
    void bar() final; // 错误:非虚函数不能被覆写或是 final
};
struct B final : A // struct B 为 final,不能被继承
{
    void foo() override; // 错误: foo 不能被覆写,因为它在 A 中是 final
};
==== Note ====
struct可与class互换;差别在于struct的默认访问属性是public




7. 继承:
公有继承的派生类定义形式:
class Derivedpublic Base{
    派生类新成员定义;
};
(1)  基类成员    在派生类中的访问属性不变。
(2)  派生类的成员函数    可以访问基类的公有成员和保护成员,不能访问基类的私有成员;
(3)  派生类以外的其它函数    可以通过派生类的对象,访问从基类继承的公有成员, 但不能访问从基类继承的保护成员和私有成员。



私有继承
私有继承的派生类定义形式: 
class Derivedprivate Base{
    派生类新成员定义;
};
(1)     基类成员    在派生类中都变成 private(2)     派生类的成员函数    可以访问基类的公有成员和保护成员,不能访问基类的私有成员;
(3)     派生类以外的其它函数    不能通过派生类的对象,访问从基类继承的任何成员。







保护继承
保护继承的派生类定义形式: 
class Derivedprotected Base{
     派生类新成员定义
};
(1)     基类成员  公有成员和保护成员变成protected,私有成员不变。
(2)     派生类的成员函数    可以访问基类的公有成员和保护成员,不能访问基类的私有成员;
(3)     派生类以外的其它函数    不能通过派生类的对象,访问从基类继承的任何成员。





8. 抽象函数(abstract functions)要求子类实现它
 
virtual double getArea() = 0;  // 在Shape类中
Circle子类必须实现getArea()纯虚函数才能实例化,且抽象类不能实例化(创建对象)



9. 动态类型转换
Dynamic Casting – Why (为何需要动态类型转换)
void printObject(Shape& shape) 
// shape是派生类对象的引用
{
  cout << "The area is " 
       << shape.getArea() << endl;
  //需求
  // 如果shape是Circle对象,就输出半径
  // 如果shape是Rectangle对象,就输出宽高
} 



dynamic_cast 运算符
(1)     沿继承层级向上、向下及侧向转换到类的指针和引用
(2)     转指针:失败返回nullptr
(3)     转引用:失败抛异常


例子
先将Shape对象用dynamic_cast转换为派生类Circle对象
然后调用派生类中独有的函数
// A function for displaying a Shape object
void printObject(Shape &shape)
{
  cout << "The area is " 
       << shape.getArea() << endl;
  Shape *p = &shape;
  Circle *c = dynamic_cast<Circle*>(p);
  // Circle& c = dynamic_cast<Circle&>(shape); 
  // 引用转换失败则抛出一个异常 std::bad_cast
  if (c != nullptr) // 转换失败则指针为空
  {
    cout << "The radius is " 
         << p1->getRadius() << endl;
    cout << "The diameter is " 
         << p1->getDiameter() << endl;
  }
}






10. Upcasting and Downcasting (向上/向下 转型):
upcasting : Assigning a pointer of a derived class type to a pointer of its base class type (将派生类类型指针赋值给基类类型指针)
downcasting : Assigning a pointer of a base class type to a pointer of its derived class type. (将基类类型指针赋值给派生类类型指针)1)Upcasting can be performed implicitly without using the dynamic_cast operator. (上转可不使用dynamic_cast而隐式转换)
Shape* s = nullptr;
Circle *c = new Circle(2);
s = c; //OK,隐式上转2)Downcasting must be performed explicitly. (下转必须显式执行)
Shape* s = new Circle(1);
Circle *c = nullptr;
c = dynamic_cast <Circle*> (s); //显式下转


====Note====
父上,子下
上转隐,下转显



11. 文件库系统:
C++17 std::filesystem provides facilities for performing operations on file systems and their components, such as paths, regular files, and directories。(标准库的filesystem提供在文件系统与其组件,例如路径、常规文件与目录上进行操作的方法)
路径类:
namespace fs = std::filesystem;
fs::path p{ "CheckPath.cpp" };


为名字空间起别名:
namespace fs = std::filesystem;

// The directory separator for Windows is a backslash (\), which needs special treat
namespace fs = std::filesystem;
fs::path p1("d:\\cpp\\hi.txt"); // 字符串中的反斜杠要被转义
fs::path p2("d:/cpp/hi.txt");   // Windows也支持正斜杠
fs::path p3(R"(d:\cpp\hi.txt)");// 使用原始字符串字面量





12. 文件输入输出流:c = cin.get(void)每次读取一个字符并把由Enter键生成的换行符留在输入队列中(scanf也是缓冲)

打开一个输出文件流的方法:
A.
std::filesystem::path p{"out.txt"};
std::ofstream output{p};

B.
std::ofstream output{"out.txt"};

C.
std::filesystem::path p{"out.txt"};
std::ofstream output{};
output.open(p);





13. 检测文件是否成功打开:
Testing if a file is successfully opened (检测文件是否成功打开)

3.1. Errors may occur (可能出现错误):

the file does not exist when reading a file (读文件时文件不存在)
the media is ReadOnly when writing a file (e.g. write to a CD) (写文件时介质只读)

3.2. To detect if a file is successfully opened: (检测文件是否正确打开的方法)

invoke fail()  immediately after open(). (open()之后马上调用fail()函数)
If fail() returns true, the file is not opened (does not exist). (fail()返回true, 文件未打开)

ofstream output("scores.txt");
if (output.fail())  {
    cout << R"(Can't open file "scores.txt"!)";
  }



14. Testing End of File (检测是否已到文件末尾)

What if you don’t know how many lines are in the file (若你不知道文件有多少行), and You want to read them all (还想把他们全读出来)?

Use eof() function to detect the end of file (eof()函数检查是否是文件末尾)
ifstream in("scores.txt");
while (in.eof() == false) {
  cout << static_cast<char>(in.get());
}

  
15. Functions for I/O Stream:
(1) getline()
1.1. When using (>>), data are delimited by whitespace. (>>运算符用空格分隔数据)
对于文件内容:
Li  Lei#Han  Meimei#Adam

如下代码只能读入“Li”
ifstream input("name.txt");
std::string name;
input >> name;

1.2. Read in "Li Lei" with member function getline(char* buf, int size, char delimiter)
constexpr int SIZE{ 40 };
std::array<char , SIZE> name{};
while (!input.eof()) {// not end of file
  input.getline(&name[ 0 ] , SIZE , '#');
  std::cout << &name[ 0 ] << std::endl;
}


1.3. Read in "Li Lei" with non-member function std::getline(istream& is, string& str, char delimiter)
 
std::string name2{};
while (!input.eof()) {
  std::getline(input, name2, '#');
  std::cout << n << std::endl;
}



(2) get() and put()
Two other useful functions are get and put.

2.1. get: read a character
int istream::get();
istream& get (char& c);

2.2. put write a character.
ostream& put (char c);



(3) flush()
3.1. Flush output stream buffer (将输出流缓存中的数据写入目标文件)
ostream& flush();
3.2. 用法
cout.flush(); // 其它输出流对象也可以调用 flush()
cout << "Hello" << std::flush; // 与endl类似作为manipulator的调用方式







15. 运算符重载:
Operators: Which can be Overloaded (可重载的运算符)
Overloadable (可重载) //不可重载的见下图

(1)     类型转换运算符:double, int, char, ……

(2)     new/delete, new []/delete[]

(3)     ""_suffix 用户自定义字面量运算符(自C++11)

(4)     一般运算符:

 Restrictions for operator overloading (运算符重载的限制)
 
(1)     Precedence and Associativity are  unchangeable (优先级和结合性不变)

(2)     NOT allowing to create new operator (不可创造新的运算符)



"""
"摘自百度知道"
运算符函数重载的返回值类型问题

参数类型:
大家都知道运算符重载函数的参数类型必须是引用类型,这是为什么呢。
拿我们最常用的赋值运算符重载来说,如果参数类型不是引用类型,那么传参时就需要一个对象的副本,
将实参复制到形参,这种复制是浅复制,并不会为形参分配内存空间,这就导致如果类中有指针类型,
形参和实参的指针就指向同一块内存,再调用析构函数时就会出错。
返回值类型:
同理返回值类型也要是引用类型,因为调用赋值运算符重载函数时也内会创建参数对象的副本,
此时也是浅复制,函数返回对象与调用函数的对象指向同一块内存,调用析构函数时会出错。
如果使用引用类型,则函数返回对象与调用函数的对象共用相同的存储空间(共用存储空间与成员指针指容向同
一块内存是不一样的),这样就不会再执行返回对象的析构函数,因而不会再出错。
"""










在这里插入图片描述

文件操作
在这里插入图片描述

流输出:在这里插入图片描述

在这里插入图片描述

有逻辑问题:这样会读到末尾eof输出后才能知道到末尾了
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值