c++ 语法 引用 多态 覆盖 重载
虚函数:实现动态多态。
纯虚函数,就是抽象类。就是接口 ABC(Abstract Base Classes)。 不能实例化。
需要实例化的子类,派生类,要重写纯虚函数。
覆盖(派生类与子类),重载(函数名相同,参数或者返回值不同),重载要求函数名相同,但是参数列表必须不同;覆盖要求函数名、参数列表、返回值必须相同。 重载描述的是同一个类中不同成员函数之间的关系;覆盖是子类和基类之间不同成员函数之间的关系。 重载的确定是在编译时确定,是静态的;虚函数则是在运行时动态确定。
以下这段是转载的,非常经典:
定义一个函数为虚函数,不代表函数为不被实现的函数。
定义他为虚函数是为了允许用基类的指针来调用子类的这个函数。
定义一个函数为纯虚函数,才代表函数没有被实现。
定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。
=====
引用变量是一个别名,也就是说,它是某个已存在变量的另一个名字。一旦把引用初始化为某个变量,就可以使用该引用名称或变量名称来指向变量。
C++ 引用 vs 指针
引用很容易与指针混淆,它们之间有三个主要的不同:
- 不存在空引用。引用必须连接到一块合法的内存。
- 一旦引用被初始化为一个对象,就不能被指向到另一个对象。指针可以在任何时候指向到另一个对象。
- 引用必须在创建时被初始化。指针可以在任何时间被初始化。
C++ 中创建引用
试想变量名称是变量附属在内存位置中的标签,您可以把引用当成是变量附属在内存位置中的第二个标签。因此,您可以通过原始变量名称或引用来访问变量的内容。例如:
int i = 17;
我们可以为 i 声明引用变量,如下所示:
int& r = i;
double& s = d;
在这些声明中,& 读作引用。因此,第一个声明可以读作 "r 是一个初始化为 i 的整型引用",第二个声明可以读作 "s 是一个初始化为 d 的 double 型引用"。
引用作为参数:
#include <iostream> using namespace std; // 函数声明 void swap(int& x, int& y); int main () { // 局部变量声明 int a = 100; int b = 200; cout << "交换前,a 的值:" << a << endl; cout << "交换前,b 的值:" << b << endl; /* 调用函数来交换值 */ swap(a, b); cout << "交换后,a 的值:" << a << endl; cout << "交换后,b 的值:" << b << endl; return 0; } // 函数定义 void swap(int& x, int& y) { int temp; temp = x; /* 保存地址 x 的值 */ x = y; /* 把 y 赋值给 x */ y = temp; /* 把 x 赋值给 y */ return; }
引用作为返回值:
C++ 把引用作为返回值
通过使用引用来替代指针,会使 C++ 程序更容易阅读和维护。C++ 函数可以返回一个引用,方式与返回一个指针类似。
当函数返回一个引用时,则返回一个指向返回值的隐式指针。这样,函数就可以放在赋值语句的左边。例如,请看下面这个简单的程序:
#include <iostream>
using namespace std;
double vals[] = {10.1, 12.6, 33.1, 24.1, 50.0};
double& setValues( int i )
{
return vals[i]; // 返回第 i 个元素的引用
}
// 要调用上面定义函数的主函数
int main ()
{
cout << "改变前的值" << endl;
for ( int i = 0; i < 5; i++ )
{
cout << "vals[" << i << "] = ";
cout << vals[i] << endl;
}
setValues(1) = 20.23; // 改变第 2 个元素
setValues(3) = 70.8; // 改变第 4 个元素
cout << "改变后的值" << endl;
for ( int i = 0; i < 5; i++ )
{
cout << "vals[" << i << "] = ";
cout << vals[i] << endl;
}
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
改变前的值
vals[0] = 10.1
vals[1] = 12.6
vals[2] = 33.1
vals[3] = 24.1
vals[4] = 50
改变后的值
vals[0] = 10.1
vals[1] = 20.23
vals[2] = 33.1
vals[3] = 70.8
vals[4] = 50
当返回一个引用时,要注意被引用的对象不能超出作用域。所以返回一个对局部变量的引用是不合法的,但是,可以返回一个对静态变量的引用。
int& func() { int q; //! return q; // 在编译时发生错误 static int x; return x; // 安全,x 在函数作用域外依然是有效的 }
https://shenjun4git.github.io/CPlusPlusCNHTML/%E7%AC%AC1%E7%AB%A0%20C++%20%E6%95%99%E7%A8%8B/119-c++-yin-yong/1191-ba-yin-yong-zuo-wei-can-shu.html
https://shenjun4git.github.io/CPlusPlusCNHTML/%E7%AC%AC1%E7%AB%A0%20C++%20%E6%95%99%E7%A8%8B/119-c++-yin-yong/1192-ba-yin-yong-zuo-wei-fan-hui-zhi.html
==============================================
C++ 存储类
存储类定义 C++ 程序中变量/函数的范围(可见性)和生命周期。这些说明符放置在它们所修饰的类型之前。下面列出 C++ 程序中可用的存储类:
-
auto:这是默认的存储类说明符,通常可以省略不写。auto 指定的变量具有自动存储期,即它们的生命周期仅限于定义它们的块(block)。auto 变量通常在栈上分配。
-
register:用于建议编译器将变量存储在CPU寄存器中以提高访问速度。在 C++11 及以后的版本中,register 已经是一个废弃的特性,不再具有实际作用。
-
static:用于定义具有静态存储期的变量或函数,它们的生命周期贯穿整个程序的运行期。在函数内部,static变量的值在函数调用之间保持不变。在文件内部或全局作用域,static变量具有内部链接,只能在定义它们的文件中访问。
-
extern:用于声明具有外部链接的变量或函数,它们可以在多个文件之间共享。默认情况下,全局变量和函数具有 extern 存储类。在一个文件中使用extern声明另一个文件中定义的全局变量或函数,可以实现跨文件共享。
-
mutable (C++11):用于修饰类中的成员变量,允许在const成员函数中修改这些变量的值。通常用于缓存或计数器等需要在const上下文中修改的数据。
-
thread_local (C++11):用于定义具有线程局部存储期的变量,每个线程都有自己的独立副本。线程局部变量的生命周期与线程的生命周期相同。
从 C++ 17 开始,auto 关键字不再是 C++ 存储类说明符,且 register 关键字被弃用。
中的存储类说明符为程序员提供了控制变量和函数生命周期及可见性的手段。
合理使用存储类说明符可以提高程序的可维护性和性能。
从 C++11 开始,register 已经失去了原有的作用,而 mutable 和 thread_local 则是新引入的特性,用于解决特定的编程问题。
=============================
class
类 & 对象详解
到目前为止,我们已经对 C++ 的类和对象有了基本的了解。下面的列表中还列出了其他一些 C++ 类和对象相关的概念,可以点击相应的链接进行学习。
概念 | 描述 |
---|---|
类成员函数 | 类的成员函数是指那些把定义和原型写在类定义内部的函数,就像类定义中的其他变量一样。 |
类访问修饰符 | 类成员可以被定义为 public、private 或 protected。默认情况下是定义为 private。 |
构造函数 & 析构函数 | 类的构造函数是一种特殊的函数,在创建一个新的对象时调用。类的析构函数也是一种特殊的函数,在删除所创建的对象时调用。 |
C++ 拷贝构造函数 | 拷贝构造函数,是一种特殊的构造函数,它在创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象。 |
C++ 友元函数 | 友元函数可以访问类的 private 和 protected 成员。 |
C++ 内联函数 | 通过内联函数,编译器试图在调用函数的地方扩展函数体中的代码。 |
C++ 中的 this 指针 | 每个对象都有一个特殊的指针 this,它指向对象本身。 |
C++ 中指向类的指针 | 指向类的指针方式如同指向结构的指针。实际上,类可以看成是一个带有函数的结构。 |
C++ 类的静态成员 | 类的数据成员和函数成员都可以被声明为静态的。 |
==================================
基类 & 派生类
一个类可以派生自多个类,这意味着,它可以从多个基类继承数据和函数。定义一个派生类,我们使用一个类派生列表来指定基类。类派生列表以一个或多个基类命名,形式如下:
class derived-class: access-specifier base-class
其中,访问修饰符 access-specifier 是 public、protected 或 private 其中的一个,base-class 是之前定义过的某个类的名称。如果未使用访问修饰符 access-specifier,则默认为 private
访问控制和继承
派生类可以访问基类中所有的非私有成员。因此基类成员如果不想被派生类的成员函数访问,则应在基类中声明为 private。
我们可以根据访问权限总结出不同的访问类型,如下所示:
访问 | public | protected | private |
---|---|---|---|
同一个类 | yes | yes | yes |
派生类 | yes | yes | no |
外部的类 | yes | no | no |
一个派生类继承了所有的基类方法,但下列情况除外:
- 基类的构造函数、析构函数和拷贝构造函数。
- 基类的重载运算符。
- 基类的友元函数。
继承类型
当一个类派生自基类,该基类可以被继承为 public、protected 或 private 几种类型。继承类型是通过上面讲解的访问修饰符 access-specifier 来指定的。
我们几乎不使用 protected 或 private 继承,通常使用 public 继承。当使用不同类型的继承时,遵循以下几个规则:
- 公有继承(public):当一个类派生自公有基类时,基类的公有成员也是派生类的公有成员,基类的保护成员也是派生类的保护成员,基类的私有成员不能直接被派生类访问,但是可以通过调用基类的公有和保护成员来访问。
- 保护继承(protected): 当一个类派生自保护基类时,基类的公有和保护成员将成为派生类的保护成员。
- 私有继承(private):当一个类派生自私有基类时,基类的公有和保护成员将成为派生类的私有成员。
======
https://www.runoob.com/cplusplus/cpp-polymorphism.html
此时,编译器看的是指针的内容,而不是它的类型。因此,由于 tri 和 rec 类的对象的地址存储在 *shape 中,所以会调用各自的 area() 函数。
正如您所看到的,每个子类都有一个函数 area() 的独立实现。这就是多态的一般使用方式。有了多态,您可以有多个不同的类,都带有同一个名称但具有不同实现的函数,函数的参数甚至可以是相同的。
虚函数
虚函数 是在基类中使用关键字 virtual 声明的函数。在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数。
我们想要的是在程序中任意点可以根据所调用的对象类型来选择调用的函数,这种操作被称为动态链接,或后期绑定。
纯虚函数
您可能想要在基类中定义虚函数,以便在派生类中重新定义该函数更好地适用于对象,但是您在基类中又不能对虚函数给出有意义的实现,这个时候就会用到纯虚函数。
我们可以把基类中的虚函数 area() 改写如下:
= 0 告诉编译器,函数没有主体,上面的虚函数是纯虚函数。
===================================
重载 ,多态 , 覆盖(重写)
1、重载(overload)
重载的定义为:在同一作用域中,同名函数的形式参数(参数个数、类型或者顺序)不同时,构成函数重载。例如:
class A
{
public:
int func(int a);
void func(int a, int b);
void func(int a, int b, int c);
int func(char* pstr, int a);
};
以上的四个函数均构成重载。
需要注意的是:
函数返回值类型与构成重载无任何关系
类的静态成员函数与普通成员函数可以形成重载
函数重载发生在同一作用域,如类成员函数之间的重载、全局函数之间的重载
对于重载,最出名的应该就是运算符重载了吧。
这里还需要注意一下 const重载:
class D
{
public:
void funcA(); //1
void funcA() const; //2
void funcB(int a); //3
void funcB(const int a); //4
};
在类D 中 funcA 与 const funcA是合法的重载,而 两个 funcB 函数是非法的,不能通过编译。
原因是:在类中,由于隐含的this形参的存在,const版本的function函数使得作为形参的this指针的类型变为指向const对象的指针,而非const版本的使得作为形参的this指针就是正常版本的指针。此处是发生重载的本质。
调用规则:const对象默认调用const成员函数,非const对象默认调用非const成员函数;
对于funcB,非引用传参,形参是否const是等价的。但是当使用引用传参时,有无const是不同的。使用指针传参时,指向const对象的指针和指向非const对象的指针做形参的函数是不同的。
2、隐藏(hiding)
隐藏定义:指不同作用域中定义的同名函数构成隐藏(不要求函数返回值和函数参数类型相同)。比如派生类成员函数隐藏与其同名的基类成员函数、类成员函数隐藏全局外部函数。
例如:
void hidefunc(char* pstr)
{
cout << "global function: " << pstr << endl;
}
class HideA
{
public:
void hidefunc()
{
cout << "HideA function" << endl;
}
void usehidefunc()
{
//隐藏外部函数hidefunc,使用外部函数时要加作用域
hidefunc();
::hidefunc("lvlv");
}
};
class HideB : public HideA
{
public:
void hidefunc()
{
cout << "HideB function" << endl;
}
void usehidefunc()
{
//隐藏基类函数hidefunc,使用外部函数时要加作用域
hidefunc();
HideA::hidefunc();
}
};
隐藏的实质是;在函数查找时,名字查找先于类型检查。如果派生类中成员和基类中的成员同名,就隐藏掉。编译器首先在相应作用域中查找函数,如果找到名字一样的则停止查找。
3、重写/覆盖(override)
重写的定义:派生类中与基类同返回值类型、同名和同参数的虚函数重定义,构成虚函数覆盖,也叫虚函数重写。
需要注意的是,这里有一个特殊情况,即协变返回类型。
定义是:如果虚函数返回指针或者引用时(不包括value语义),子类中重写的函数返回的指针或者引用是父类中被重写函数所返回指针或引用的子类型。看示例代码:
class Base
{
public:
virtual A& show()
{
cout<<"In Base"<<endl;
return *(new A);
}
};
class Derived : public Base
{
public:
//返回值协变,构成虚函数重写
B& show()
{
cout<<"In Derived"<<endl;
return *(new B);
}
};
对比覆盖和隐藏,不难发现函数覆盖其实是函数隐藏的特例。如果派生类中定义了一个与基类虚函数同名但是参数列表不同的非virtual函数,则此函数是一个普通成员函数,并形成对基类中同名虚函数的隐藏,而非虚函数覆盖。
隐藏是一个静态概念,它代表了标识符之间的一种屏蔽现象,而覆盖则是为了实现动态联编,是一个动态概念。
4、final和override说明符
通过上面的介绍,我们知道派生类可以定义一个函数与基类中虚函数的名字相同但是形参列表不同的函数(隐藏)。编译器将认为新定义的这个函数与基类中原有的函数是相互独立的。
但是我们在写虚函数时,想让派生类中的虚函数覆盖掉基类虚函数,有时我们会不小心写错,造成了隐藏,这不是我们想要看到的结果。所以C++ 11新标准中我们可以使用override关键字来说明派生类中的虚函数。
如果我们使用override标记了某个函数,但该函数并没有覆盖已存在的虚函数,此时编译器将报错。
class B
{
virtual void f1( int ) const;
virtual void f2();
void f3();
};
class C : B
{
void f1( int ) const override; //正确,f1与基类中的f1匹配
void f2( int ) override; //错误:B没有形如f2(int)的函数
void f3() override; //错误:f3不是虚函数
void f4() override; //错误:B中没有名为f4的函数
};
使用override是希望能覆盖基类中的虚函数,如果不符合则编译器报错。
我们还能把某个函数指点为 final ,意味着任何尝试覆盖该函数的操作都将引发错误:
class D : B
{
//从B继承 f2() 和 f3(),覆盖 f1( int )
void f1( int ) const final; //不允许后续的其它类覆盖 f1(int)
};
class E : D
{
void f2(); //正确:覆盖从间接类B继承而来的f2
void f1( int ) const; //错误:D已经将 f2 声明成 final
};
final 和 override 说明符出现在形参列表以及尾置返回类型之后。
final 还可以跟在类的后面,意思这个类不能当做其它类的基类。
5、总结
在讨论相关概念的区别时,抓住定义才能区别开来。C++中函数重载、隐藏和覆盖的区别并不难。
在这里,牢记以下几点,就可区分函数重载、函数隐藏、函数覆盖和函数重写的区别:
函数重载发生在相同作用域
函数隐藏发生在不同作用域
函数覆盖就是函数重写。准确地叫做虚函数覆盖和虚函数重写,也是函数隐藏的特例
关于三者的对比,如下表所示:
感谢大家,我是假装很努力的YoungYangD(小羊)。
参考资料:
《C++ primer》
https://www.cnblogs.com/xiaozz/p/6448396.html
————————————————
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/weixin_39640298/article/details/88725073
====================
接口,纯虚方法,就是接口的方法。要实现的方法
接口描述了类的行为和功能,而不需要完成类的特定实现。
C++ 接口是使用抽象类来实现的,抽象类与数据抽象互不混淆,数据抽象是一个把实现细节与相关的数据分离开的概念。
如果类中至少有一个函数被声明为纯虚函数,则这个类就是抽象类。纯虚函数是通过在声明中使用 "= 0" 来指定的,如下所示:
设计抽象类(通常称为 ABC)的目的,是为了给其他类提供一个可以继承的适当的基类。抽象类不能被用于实例化对象,它只能作为接口使用。如果试图实例化一个抽象类的对象,会导致编译错误。
因此,如果一个 ABC 的子类需要被实例化,则必须实现每个纯虚函数,这也意味着 C++ 支持使用 ABC 声明接口。如果没有在派生类中重写纯虚函数,就尝试实例化该类的对象,会导致编译错误。
可用于实例化对象的类被称为具体类。
https://www.runoob.com/cplusplus/cpp-interfaces.html
=============
#include <iostream> #include <string> using namespace std; class Animal { public: virtual void eat(); void sleep(); }; void Animal::eat(){ cout << "animal i'am eat" << endl; } void Animal::sleep(){ cout << "animal i am sleep" << endl; } class Dog : public Animal{ public: void bark(){ cout << "dog i am bark" << endl; } void sleep(){ cout << "dog sleep" << endl; } void eat(){ cout << "dog eat" << endl; } }; class Cat : public Animal{ public: void bark(){ cout << "cat bark" << endl; } void sleep(){ // override 重写 覆盖 cout << "cat sleep" << endl; } void sleep(int i){ cout << "cat sleep " << i << " days" << endl; } void eat(){ cout << "cat eat" << endl; } }; class Box { public: int getVolume(){ return length*height*breadth; } void setLength(int len){ length = len; } void setHeight(int len){height = len;} void setBreadth(int len){breadth = len;} Box operator +(const Box& b){ Box box; box.length=this->length + b.length; box.height=this->height + b.height; box.breadth=this->breadth + b.breadth; return box; } Box (){cout << "box 构造函数" << endl;} ~Box(){cout << "box 析构函数" << endl;} private: int length; int height; int breadth; }; class Shape{ // ABC public: virtual int getArea() = 0; // virtual 纯虚函数 void setWidth(int len){width = len;} void setHeight(int len){height = len;} protected: int width; int height; }; class Rectangle : public Shape{ public: int getArea(){return width * height;} }; class Triangle : public Shape{ public: int getArea() {return (width * height) / 2;} }; namespace myfirst_space { void func(){cout << "myfirst_space func" << endl;} } void func(){ cout << "func" << endl; } int main(void){ cout << "hello world" << endl; string s1("haha"); cout << "s1:" << s1 << endl; Animal a; // cout << a.eat() << endl; Cat cat1; a.eat(); a.sleep(); Dog dog1; dog1.bark(); dog1.sleep(); cat1.sleep(); cat1.sleep(5); // overload 重载 a.eat(); cat1.eat(); dog1.eat(); Animal * a1; a1=&dog1; a1->eat(); // 动态多态 a1->sleep(); // 静态多态,早绑定 a1=&cat1; a1->eat(); a1->sleep(); Box b1,b2; b1.setHeight(10); b1.setLength(20); b1.setBreadth(30); b2.setHeight(10); b2.setLength(20); b2.setBreadth(30); Box b3 = b1 + b2; cout << "volume:" << b3.getVolume() << endl; // cout << dog1.bark() << dog1.sleep() << endl; Rectangle Rect; Triangle Tri; Rect.setWidth(10); Rect.setHeight(20); Tri.setWidth(10); Tri.setHeight(20); cout << "rectangle area:" << Rect.getArea() << " triangle area:" << Tri.getArea() << endl; // new delete Box * boxArray= new Box[2]; delete [] boxArray; // namespace 命名空间 func(); myfirst_space::func(); return 0; }
========
参考:
https://www.runoob.com/cplusplus/cpp-references.html
https://www.runoob.com/cplusplus/cpp-classes-objects.html