系列文章目录
文章目录
动力猿在备考研究生考试时总结的C++考点,通俗易懂,突出重点,能应对绝大多数学校的期末考试和研究生复试,如果对您有所帮助还请点赞关注支持一下动力猿!
一、类和对象
头文件和源文件
对于较复杂的C++程序,通常会将类的定义放在.h头文件中。普通函数、成员函数以及main函数放在.c文件中。
类定义后必须有;结束
改错题的一个考点
类的封装性(public、protected、private)
在C++中,public、private和protected是访问控制修饰符,用于控制类的成员的访问权限。这些修饰符定义了类成员对外部世界的可见性和可访问性。
下面是对这些访问控制修饰符的作用的简要说明:
-
public:公有成员在类的内部可以随便访问且前面不需要加"对象."。外部也可以通过“本类对象."直接访问。
-
protected:保护成员在类的内部可以随便访问且前面不需要加"对象."。外部无法通过对象直接访问,可以通过本类对象调用公有成员函数间接访问。
-
private:私有成员在类的内部可以随便访问且前面不需要加"对象."。外部无法通过对象直接访问,可以通过本类对象调用公有成员函数间接访问。
一个类如果不当基类,类的所有数据都使用private修饰,然后通过对象调用public下特定的公共成员函数访问这些private数据。
基类的数据不用private而用protected修饰因为需要让派生类也能访问到。基类的成员函数也放在public下,一般都有虚函数。
这表明我们可以通过公有成员函数间接访问私有和保护成员,而无法直接访问它们。这种访问控制机制确保了数据的封装和安全性,同时提供了公有接口来操作类的成员。
在类定义中,成员的缺省访问属性是private,这体现了封装性。
#include <iostream>
using namespace std;
class MyClass {
public: // 公有成员
int publicData;
private: // 私有成员
int privateData;
protected: // 保护成员
int protectedData;
public:
void setPrivateData(int data) {
privateData = data; // 在类的内部可以访问私有成员
}
int getPrivateData() {
return privateData; // 在类的内部可以访问私有成员
}
void setProtectedData(int data) {
protectedData = data; // 在类的内部可以访问保护成员
}
int getProtectedData() {
return protectedData; // 在类的内部可以访问保护成员
}
};
int main() {
MyClass obj;
obj.publicData = 10; // 可以直接访问公有成员
cout << "Public data: " << obj.publicData << endl;
// obj.privateData = 20; // 错误:无法访问私有成员
// obj.protectedData = 30; // 错误:无法访问保护成员
obj.setPrivateData(20); // 通过公有成员函数访问私有成员
cout << "Private data: " << obj.getPrivateData() << endl;
obj.setProtectedData(30); // 通过公有成员函数访问保护成员
cout << "Protected data: " << obj.getProtectedData() << endl;
return 0;
}
类里面的函数(成员函数)
内联成员函数
调用函数需要开销(跳转到函数定义处、参数值传递、返回值的拷贝等),对于简单的函数如果能在编译的时候直接放到每个调用它的地方就能节省调用函数的开销,这就叫内联函数,本来是通过在函数返回类型前加inline声明。
类会把所有直接放在它体内的函数体都隐式设置为内联函数。当然也可以不写在类里面而是在类外部显式声明。虽然你可以将函数声明为内联函数,但是最终是否进行内联处理,还是由编译器决定的。如果编译器认为将函数进行内联处理不会提高效率,那么编译器可以选择忽略这个请求。
内联函数的定义必须出现在第一次调用之前,其他普通的函数只需要原型声明出现在第一次调用前就行而定义可以在调用之后。
class Clock{
public:
//隐式声明内联函数
void showTime(){cout<<hour<< ": "<<minute<<" :"<<second<<endl;}
private:
int hour, minute, second;
};
/*显式声明内联函数
inline void Clock : :showTime(){cout<<hour<< ": "<<minute<<":"<<second<<endl};
*/
构造函数
构造函数在创建对象时被自动调用,用来完成对象的初始化。构造函数体里可以放其他成员函数!
构造函数必须和类同名而且没有返回值。
一般我们都用提供参数的构造函数来初始化对象的属性值。不提供参数的构造函数称为默认构造函数。如果类中什么构造函数都没有,编译器就自动生成一个隐含的默认构造函数。一个类里可以同时有这两种构造函数,这就是构造函数的重载。
当一个类的构造函数和析构函数都没有任何实现时,我们称之为空构造函数和空析构函数。下面是一个示例代码:
需要注意,构造函数和析构函数就算为空,也必须有{}。如果没有{},就是光声明而没有定义就会报错。
class MyClass {
public:
// 空构造函数
MyClass() {}
// 空析构函数
~MyClass() {}
};
在上面的示例中,MyClass
类具有一个空的构造函数和一个空的析构函数。构造函数不包含任何参数,并且没有任何实现代码。析构函数也没有任何参数,并且没有任何实现代码。这样的类可以用于简单的数据结构或者作为其他类的基类。
复制构造函数
将一个已存在的同类对象的引用给构造函数做形参,就得到复制构造函数。
调用复制构造函数能直接复制已存在的形参对象得到本对象而不用像构造函数一样一个一个属性地初始化。类中没有复制构造函数系统也会需要时自动生成一个隐含的复制构造函数。
构造函数是在对象被创建时调用,而复制构造函数是在以下三种需要复制对象的情况调用:
1.当函数的形参是类的对象;
2.函数的返回值是类的对象;
3.对象需要通过另外一个对象进行初始化。(注意只能是初始化而不是后来赋值!)
浅拷贝是指当对象被复制时,只是简单地将原始对象的指针赋值给新对象。这意味着两个对象将共享同一个指针,指向相同的内存。一个对象修改时另一个对象也会受到影响。
常要求复制构造函数实现深拷贝,深拷贝是指为新对象创建一个新的指针,并将原始对象指针指向的内存复制到新对象的指针中。这样,两个对象将拥有独立的、相同值的内存。由于拥有各自独立的内存空间,修改一个对象不会影响另一个对象。
下面的程序里,在Set类的复制构造函数中,我们首先将原对象的size
赋值给新对象。然后,我们使用new
运算符为新对象的elements
成员动态分配内存。接下来,我们使用循环将原对象的元素逐个拷贝到新对象的数组中。这样每个Set对象都有独立的整数数组,而不是共享同一个数组,各自独立地添加、删除和修改元素而不会相互干扰。
一个类的定义中,可以直接访问该类任何一个对象的私有成员,而不光是this的私有成员。所以在复制构造函数中,我们可以直接访问传入对象(形参set)的私有数据成员。这是因为复制构造函数是Set类的成员函数,它可以访问任何Set类对象的私有成员。
但如果你在Set类定义的外部试图访问其私有成员,就会导致编译错误。私有成员只能在其所属类的成员函数(包括该类的构造函数、析构函数、成员函数)及友元函数中访问。这是封装的一部分,是面向对象设计的基本原则之一,用于保护数据的安全性和完整性。
#include <iostream>
using namespace std;
class Set {
private:
int* p; // 整数数组指针
int size; // 数组大小
public:
Set() {
size = 0;
p = NULL;
}
// 复制构造函数
Set(const Set& otherSet) {
size = otherSet.size;
if(otherSet.size==0){
p=NULL;
}
else{
p = new int[size];
for (int i = 0; i < size; i++) {
p[i] = otherSet.p[i];
}
}
}
void add(int num) {
// 检查是否已经存在于集合中
for (int i = 0; i < size; i++) {
if (p[i] == num) {
return; // 已经存在,不需要添加
}
}
//集合为空添加数据
if(size==0){p=new int [1];p[0]=num;}
else{
//新动态分配一个多一位的数组代替原数组
int* p1 = new int[size];
for (int i = 0; i < size; i++) {
p1[i] = p[i];
}
p1[size] = num;
delete[] p;//成员数组指向新内存后到时候析构函数删的就是新内存,所以在这之前先得把旧内存释放掉
p = p1;
}
size++;
}
int getSize(){return size;}
~Set() {
delete[] p;
}
};
int main() {
Set set1;
set1.add(5);
set1.add(2);
set1.add(8);
cout<<"集合1的长度为"<<set1.getSize()<<endl;
Set set2 = set1; // 测试复制构造函数
set2.add(10);
set2.add(5);
cout<<"集合2的长度为"<<set2.getSize()<<endl;
return 0;
}
析构函数
析构函数是在对象的生存期即将结束的时刻被自动调用的,用来完成对象被删除前的一些扫尾工作,它的调用完成之后,对象也就消失了,相应的内存空间也被释放。
它的名称是由类名前面加“~”构成,没有返回值和参数但可以是虚函数。
#include <iostream>
class MyClass {
public:
MyClass() {
std::cout << "Constructor called" << std::endl;
}
~MyClass() { } // 大括号必须有,就算没具体的内容
};
如果不进行显式说明,系统也会生成一个函数体为空的隐含析构函数。
类的组合
通过将Point
对象作为LineSegment
类的成员变量,我们实现了类之间的组合关系。这样,一个线段对象就包含了两个点对象,形成了整体与部分的关系。通过组合,LineSegment
类可以利用Point
类的功能来计算线段的长度和判断点是否在线段上。
#include <iostream>
#include <cmath>
using namespace std;
class Point {
private:
double x;
double y;
public:
// 构造函数,用于初始化点的坐标
Point(double xCoord, double yCoord) : x(xCoord), y(yCoord) {}
// 获取点的x坐标
double getX() {
return x;
}
// 获取点的y坐标
double getY() {
return y;
}
};
class LineSegment {
private:
Point startPoint;
Point endPoint;
public:
// 构造函数,用于初始化线段的起点和终点
LineSegment(Point start, Point end) : startPoint(start), endPoint(end) {}
// 获取线段的长度
double getLength() {
double dx = endPoint.getX() - startPoint.getX();
double dy = endPoint.getY() - startPoint.getY();
return sqrt(dx * dx + dy * dy);
}
// 重载大于运算符,用于比较两条线段的长度
bool operator>(LineSegment other) {
return getLength() > other.getLength();
}
// 判断点是否在线段上:在线段上的点到起点终点距离和是定值
bool contains(Point point) {
LineSegment l1(startPoint,point);
LineSegment l2(endPoint,point);
return (l1.getLength()+l2.getLength())==getLength();
}
};
int main() {
Point p1(0, 0);
Point p2(3, 4);
Point p3(5, 12);
LineSegment line1(p1, p2);
LineSegment line2(p1, p3);
cout << "Length of line1: " << line1.getLength() << endl;
cout << "Length of line2: " << line2.getLength() << endl;
if (line2 > line1) {
cout << "line2 is longer than line1" << endl;
} else {
cout << "line2 is not longer than line1" << endl;
}
return 0;
}
类的友元
通俗地说,友元关系就是一个类主动声明哪些其他类或函数是它的朋友,进而给它们提供对本类的访问特许。
友元的缺点是对数据隐蔽性和封装的破坏。
友元的优点是通过数据共享提高了程序的效率和可读性。
友元函数是在类中用关键字friend声明的,一旦是该类的友元函数就不可能是该类的成员函数了。可以是普通函数或其他类的成员函数。友元函数可以在类的内部定义也可以在外部定义。一个类声明某函数是它的友元函数,那么这个友元函数的形参表里应该有这个类的对象,友元函数体里可以像在该类内一样随意访问该类的私有和保护成员。
#include <iostream>
class MyClass {
private:
int privateData;
protected:
int protectedData;
public:
MyClass() : privateData(10), protectedData(20) {}
friend void friendFunc(MyClass obj);
};
void friendFunc(MyClass obj) {
std::cout << "Accessing private data: " << obj.privateData << std::endl;
std::cout << "Accessing protected data: " << obj.protectedData << std::endl;
}
int main() {
MyClass obj;
friendFunc(obj);
return 0;
}
友元类
同友元函数一样,一个类可以将另一个类声明为友元类。若A类声明B类是它的友元类,则是给了B类足够的信任,这样B类的所有成员函数都是A类的友元函数都可以直接访问A类的私有和保护成员。
class A{
public:
void display() {cout<<x<<endl;}
friend class B;//A类声明B类为友元类
//其他成员略
private:
int x;
}
class B{
public:
void set (int i);
void display();
private:
A a;
};
void B::set(int i){
a.x=i;//由于B是A的友元,所以在B的成员函数中可以访问A类对象的私有成员
}
//其他函数的实现略
关于友元需要注意:
第一,友元关系是不能传递的。B类是A类的友元,C类是B类的友元,但C类不一定是A类的友元。
第二,友元关系是单向的。如果声明B类是A类的友元,B类的成员函数就可以访问A类的私有和保护数据,但A类的成员函数却不能访问B类的私有、保护数据。
第三,友元关系是不被继承的。如果类B是类A的友元,类B的派生类并不会自动成为类A的友元。就好像别人信任你,但是不见得信任你的孩子。
二、继承与派生
派生类能不能访问基类成员
从基类继承过去的成员在儿子就是儿子的了,但是其身份会发生变化,这决定了派生类能不能访问该基类成员。这个变化是由三种不同的派生方式公有派生、保护派生、私有派生决定的。
不管那种派生方式,基类的private数据成员根本就继承不到派生类里去用任何手段都是无法访问的,所以基类的数据成员都是protected而不能是private。三种不同的派生方式公有派生、保护派生、私有派生决定的是基类的public和protected数据成员在派生类里的身份。
公有继承:基类public——派生类public 基类protected——派生类protected
保护继承:基类public——派生类protected 基类protected——派生类protected
私有继承:基类public——派生类private 基类protected——派生类private
#include <iostream>
using namespace std;
// 基类 Animal
class Animal {
protected:
int age; // 保护数据成员
private:
float weight; // 私有数据成员
public:
string name;
Animal(string _name, int _age, float _weight) : name(_name), age(_age), weight(_weight) {}
virtual void makeSound() {
cout << "Animal makes sound" << endl;
}
void printInfo() {
cout << "Name: " << name << endl;
cout << "Age: " << age << endl;
cout << "Weight: " << weight << endl; // 输出私有数据成员
}
};
// 派生类 Dog(公有继承)
class Dog : public Animal {
public:
Dog(string _name, int _age, float _weight) : Animal(_name, _age, _weight) {}
void makeSound() override {
cout << "Dog barks" << endl;
}
void printInfo() {
// 基类的保护数据成员和公有数据成员身份不变
cout << "Name: " << name << endl;
cout << "Age: " << age << endl;
// cout << "Weight: " << weight << endl; // 无法访问私有数据成员
}
};
// 派生类 Cat(保护继承)
class Cat : protected Animal {
public:
Cat(string _name, int _age, float _weight) : Animal(_name, _age, _weight) {}
void makeSound() override {
cout << "Cat meows" << endl;
}
void printInfo() {
// 基类的保护数据成员和公有数据成员都以保护身份出现
cout << "Name: " << name << endl;
cout << "Age: " << age << endl;
// cout << "Weight: " << weight << endl; // 无法访问私有数据成员
}
};
// 派生类 Rabbit(私有继承)
class Rabbit : private Animal {
public:
Rabbit(string _name, int _age, float _weight) : Animal(_name, _age, _weight) {}
void makeSound() override {
cout << "Rabbit squeaks" << endl;
}
void printInfo() {
//基类的保护数据成员和公有数据成员都以私有身份出现
cout << "Name: " << name << endl;
cout << "Age: " << age << endl;
// cout << "Weight: " << weight << endl; // 无法访问私有数据成员
}
};
int main() {
Dog* animal1 = new Dog("Buddy", 3, 10.5);
Cat* animal2 = new Cat("Whiskers", 2, 5.2);
Rabbit* animal3 = new Rabbit("Bugs", 1, 2.7);
cout << "Using Dog class:" << endl;
animal1->printInfo(); // 调用 Dog 类的成员函数
cout << "\nUsing Cat class:" << endl;
animal2->printInfo();
cout << "\nUsing Rabbit class:" << endl;
animal3->printInfo();
return 0;
}
上面的继承规则不光适用于基类数据成员也适用于基类函数成员。特殊的是当派生类中的函数和它继承的基类中的函数同名时,派生类的函数会隐藏基类的同名函数。但是,我们可以使用范围解析运算符 ::
来调用基类中的被隐藏的同名函数。
以下是一个示例代码:
#include <iostream>
class Base {
public:
void func() {
std::cout << "Base::func() called" << std::endl;
}
};
class Derived : public Base {
public:
void func() {
std::cout << "Derived::func() called" << std::endl;
}
};
int main() {
Derived d; // 创建派生类对象
d.func(); // 调用派生类的同名函数
d.Base::func(); // 使用范围解析运算符 :: 调用基类的同名函数
return 0;
}
派生类的构造函数
派生类构造函数的执行次序如下:
①调用基类构造函数,若有多个基类,调用顺序按照他们的继承时声明的顺序
②调用内嵌成员对象的构造函数,调用顺序按照他们在类中声明的顺序。
③派生类的构造函数体中的内容
析构函数的执行次序相反。
派生类的赋值兼容规则
派生类的赋值兼容规则主要包括以下几个方面:
-
派生类对象可以赋值给基类对象。
-
派生类的指针可以赋值给基类的指针。
-
派生类的引用可以赋值给基类的引用。
下面给出相应的代码示例来说明这些规则:
- 派生类对象可以赋值给基类对象:派生类对象赋值给基类对象时,会进行对象切片,只会保留基类部分的成员。
class Base {
public:
int x;
};
class Derived : public Base {
public:
int y;
};
int main() {
Derived d;
d.x = 10;
d.y = 20;
Base b = d; // 派生类对象赋值给基类对象
cout << b.x << endl; // 输出:10
return 0;
}
- 派生类的指针可以赋值给基类的指针:派生类指针或引用赋值给基类指针或引用时,通过基类指针或引用只能访问到派生类重写的基类虚函数,而不能访问到派生类的任何其他成员了。
class Base {
public:
int x;
};
class Derived : public Base {
public:
int y;
};
int main() {
Derived d;
d.x = 10;
d.y = 20;
Base* bPtr = &d; // 派生类指针赋值给基类指针
cout << bPtr->x << endl; // 输出:10
return 0;
}
- 派生类的引用(或对象,因为引用本来就是对象的别名)可以赋值给基类的引用:派生类指针或引用赋值给基类指针或引用时,通过基类指针或引用只能访问到派生类重写的基类虚函数,而不能访问到派生类的任何其他成员了。
class Base {
public:
int x;
};
class Derived : public Base {
public:
int y;
};
int main() {
Derived d;
d.x = 10;
d.y = 20;
Base& bRef = d; // 派生类引用赋值给基类引用
cout << bRef.x << endl; // 输出:10
return 0;
}
虚基类
虚基类主要用于解决多重继承中的访问二义性问题。
派生类如果从多个路径继承同一个基类,那么同一个基类子对象在派生类里就会有不同层次的多个,访问时就会产生二义性问题。而虚基类的成员在派生类中只存在一份拷贝,避免了派生类中对同一基类成员进行多份拷贝。
另外,实例化对象时,只有直接派生类的构造函数会调用虚基类的构造函数,而不是所有派生类的构造函数都会调用虚基类构造函数。
我们有一个基类Base
和三个派生类Derived1
、Derived2
和Derived3
。Derived1
和Derived2
都使用了虚继承,它们都继承了Base
类。Derived3
继承了Derived1
和Derived2
。通过使用虚继承,Derived3
类只包含一个Base
类的子对象,而不是两个副本。
#include <iostream>
using namespace std;
class Base {
public:
int data;
};
class Derived1 : virtual public Base { // 使用虚继承
public:
void setData(int value) {
data = value;
}
};
class Derived2 : virtual public Base { // 使用虚继承
public:
void displayData() {
cout << "Data: " << data << endl;
}
};
class Derived3 : public Derived1, public Derived2 {
public:
void showData() {
cout << "Data: " << data << endl;
}
};
int main() {
Derived3 obj;
obj.setData(5);
obj.displayData();
obj.showData();
return 0;
}
三、多态
函数重载
函数名相同,形参的个数或者类型或者顺序不同就叫做函数的重载。
编译器判断重载函数的依据:
形参个数、类型、顺序
常成员函数const类型也可用于重载
编译器不能通过以下判断重载函数:
形参名
返回值类型
所以仅修改这两项编译器会报错有两个相同函数!
在类里面写运算符重载函数
运算符重载函数的参数都是类或类的引用,实现类对象之间的运算。
写成友元运算符重载函数
一个类要声明一个友元运算符重载函数给这个运算符重载函数以把自己当数用的权利。
函数参数表里肯定有这个类的对象而且参数的数量就是该运算符的目。
返回值类型看运算符,如果是计算值的运算符如加减乘除余自增自减,返回值类型就是类或者类的引用(见自增运算)。如果是比较运算符,返回值类型肯定是bool。
//形参里就采用 常引用const 类名 &,课本上是这样写的
#include<iostream>
using namespace std;
class Example {
int value;
public:
Example(int val): value(val) {}
// 声明友元函数
friend Example operator%(Example const &, Example const &);
friend Example& operator++(Example &);
friend bool operator>=(Example const &, Example const &);
};
// %运算符重载
Example operator%(Example const &a, Example const &b) {
return Example(a.value % b.value);
}
// ++运算符重载
Example& operator++(Example &a) {
++a.value;
return a;
}
// >=运算符重载
bool operator>=(Example const &a, Example const &b) {
return a.value >= b.value;
}
int main() {
Example a(10), b(3);
Example c = a % b; // 使用重载的%运算符
++a; // 使用重载的++运算符
cout << "a >= b: " << (a >= b) << endl; // 使用重载的>=运算符
return 0;
}
写成成员运算符重载函数
成员函数有个得天独厚的优势就是可以任意访问本类的数据成员。
函数参数表里参数的数量要比该运算符的目少1,也就是说本对象就算一个隐含参数了。
返回值类型看运算符,如果是计算值的运算符如加减乘除余自增自减,返回值类型就是类或者类的引用(见自增运算)。如果是比较运算符,返回值类型肯定是bool。
//形参里就采用 常引用const 类名 &,课本上是这样写的
#include<iostream>
using namespace std;
class Example {
int value;
public:
Example(int val): value(val) {}
// %运算符重载
Example operator%(Example const &b) {
return Example(this->value % b.value);
}
// ++运算符重载
Example& operator++() {
++this->value;
return *this;
}
// >=运算符重载
bool operator>=(Example const &b) {
return this->value >= b.value;
}
};
int main() {
Example a(10), b(3);
Example c = a % b; // 使用重载的%运算符
++a; // 使用重载的++运算符
cout << "a >= b: " << (a >= b) << endl; // 使用重载的>=运算符
return 0;
}
虚函数和抽象类
虚函数和抽象类的概念
虚函数(Virtual Functions)是C++中的一种机制,用于实现运行时多态性(Runtime Polymorphism)。纯虚函数是通过在函数声明中使用 “= 0” 来声明的虚函数(函数体为空{},但是可以有形参)。
带有纯虚函数的类称为抽象类,抽象类不能被实例化,因此抽象类不能用作返回值类型和形参类型。
所以要使用抽象类的对象,通常需要通过派生类来声明对象,允许在派生类中重写基类的同名同参同返回值的虚函数,通过基类指针或引用调用虚函数时,根据实际对象的类型来确定调用哪个虚函数,这样能够以一种统一的方式处理不同类型的对象。
使用基类指针指向派生类对象的地址时,因为是个指针所以调用重写的虚函数用->
使用基类引用指向派生类对象时,因为引用就是变量这里是对象的别名,调用重写的虚函数用.就行
派生类中的同名函数是否是重写的虚函数?
当派生类实现抽象类的虚函数叫做函数重写而不是重载,重写必须保持函数的形参列表和返回值类型与抽象类中的虚函数一致只是重写函数体里的内容。
下面是一段代码证明:
#include <iostream>
using namespace std;
class Base {
public:
virtual void A(int x) {
cout << "Base::A(int)" << endl;
}
};
class Derived : public Base {
public:
void A(double y) { // 这并不是重写Base类中的A函数,而是Derived类的一个新的成员函数
cout << "Derived::A(double)" << endl;
}
};
int main() {
Derived d;
Base& b = d; //基类指针或引用指向派生类对象
b.A(10); //这里调用的仍然是Base::A(int),而不是Derived::A(double)
return 0;
}
在上述代码中,虽然Derived类中有一个函数A的名字与基类中的虚函数A相同,但是参数列表不同,因此Derived类中的A并不重写Base类中的A,而是Derived类的一个新的成员函数。所以,当我们通过基类的指针或引用调用A函数时,仍然会调用Base类中的A函数,而不是Derived类中的A函数。
下面是一个派生类正确重写基类虚函数的示例:
#include <iostream>
class Base
{
public:
virtual void foo()
{
std::cout << "Base::foo called\n";
}
};
class Derived : public Base
{
public:
// 这里的foo函数默认就是虚函数,因为在基类中已经声明过了
void foo() //override
{
std::cout << "Derived::foo called\n";
}
};
int main()
{
Derived derived;
Base* basePtr = &derived;
// 这里调用的是Derived::foo,尽管通过的是Base类型的指针
basePtr->foo();
return 0;
}
在这个例子中,Base
类有一个虚函数foo
,在Derived
类中我们也定义了一个同名的函数foo
。即使我们没有在Derived
类中的foo
函数前面显式地写上virtual
关键字,这个函数依然是虚函数,因为在基类Base
中它已经被声明为虚函数了。
当我们通过基类指针basePtr
调用foo
函数时,实际上调用的是Derived
类中的版本,这就是多态的体现。
类型转换打破赋值兼容规则——用基类指针或引用调用派生类的独有成员函数
由赋值兼容规则我们知道,不能直接通过基类的指针或引用调用派生类独有的函数,只能直接调用基类自己的成员函数或虚函数。
那么如果非要需要通过基类的指针或引用调用派生类独有的函数呢?这需要进行类型转换(通常是动态类型转换,dynamic_cast)。
以下是一个示例代码:
#include <iostream>
class Animal {
public:
virtual void makeSound() const = 0;
};
class Dog : public Animal {
public:
void makeSound() const override {
std::cout << "Woof!" << std::endl;
}
void fetch() const {
std::cout << "Fetching a ball!" << std::endl;
}
};
int main() {
Dog dog;
Animal* animalPtr = &dog;
animalPtr->makeSound(); // 可以调用,因为 makeSound 是 Animal 类的成员函数
// animalPtr->fetch(); // 错误,fetch 函数不是 Animal 类的成员函数
// 需要进行类型转换才能调用派生类独有的函数
Dog* dogPtr = dynamic_cast<Dog*>(animalPtr);
if (dogPtr) {
dogPtr->fetch(); // 成功调用
}
return 0;
}
在上面的示例中,我们可以通过Animal
类的指针animalPtr
调用makeSound
函数,因为makeSound
函数是Animal
类的成员函数。
但是,我们不能直接通过animalPtr
调用fetch
函数,因为fetch
函数是Dog
类的成员,而不是Animal
类的成员。如果我们想要调用Dog
类的fetch
函数,我们需要将animalPtr
进行类型转换,转换为Dog
类的指针。
在这个示例中,我们使用dynamic_cast
进行类型转换,然后通过转换后的dogPtr
调用fetch
函数。这样,我们就可以调用Dog
类的fetch
函数了。
输出将是:
Woof!
Fetching a ball!
四、数组、字符串
字符串
字符数组和字符串一定要加“”,这是改错题的一个考点。
字符串有三种表示方式:字符数组、字符指针、string
字符数组
字符数组一共有数组和指针两类写法,需要注意空’\0’也算一位。
字符数组的初始化方式:
字符数组的初始化必须使用字符串常量或字符数组来进行,而不能直接使用字符指针。
//使用字符串常量初始化
char arr[] = "Hello";
char arr[6] = "Hello";
//使用字符数组初始化
char arr[] = {'H', 'e', 'l', 'l', 'o', '\0'};
char arr[6] = {'H', 'e', 'l', 'l', 'o', '\0'};
如果你想将字符指针 a
所指向的字符串内容复制到字符数组 b
中,只能使用字符串库中的函数(如 strcpy
)来完成这个任务。
#include <iostream>
#include <cstring>
int main() {
const char* a = "Hello"; // 字符指针指向字符串常量
char b[20]; // 字符数组
std::strcpy(b, a); // 使用strcpy将字符串内容复制到字符数组
std::cout << "a: " << a << std::endl;
std::cout << "b: " << b << std::endl;
return 0;
}
字符指针
char* arr = "Hello";
string
读入带空格的字符串
五、const与static
const
与类无关的const
const修饰的变量的值必须在声明时初始化且在初始化后便不能修改,因此重点在于const修饰的是什么。
const int a是一个整型常量,其值初始化后不能修改。
const int * a是一个指向常量的指针(const 修饰的是int,a的值是常量),指针指向的值是一个常量,不能直接改变指针指向的地址中的值,但可以改变指针指向的地址。
int * const a是常量指针(const修饰的是 *,a的地址是常量),常量指针必须初始化,一旦初始化完成,它的值不能改变,即它指向的地址不能改变,但它所指向地址中的值可以改变。
const int &a是常引用,等价于const int* const(const同时修饰int和*,a的地址和值都是常量),它指向的地址中的值不能改变,它指向的地址也不能改变。
int const &a是常引用的另一种形式,等价于const int &a。
与类有关的const
与类有关的const是用来保护数据的。
一个类的对象可以被声明为常对象。
class A{
public:
A(int i,int j):x(i),y(j){}
...
private:
int x,y;
};
const A a(3,4);//常对象初始化后再也不能被赋值
}
常对象为什么安全,因为它只能通过常成员函数来对内部进行访问。
当成员函数与常成员函数同名,常对象会调用常成员函数,普通对象会调用成员函数。
#include<iostream>
using namespace std;
class R{
public:
R(int r1,int r2):r1(r1),r2(r2){}
void print();
void print() const;
private:
int r1,r2;
};
void R::print(){
cout<<r1<< ":"<<r2<<endl;
}
void R:: print() const {
cout<<r1<<"; "<<r2<<endl;
}
int main(){
R a(5,4);
a.print();
//调用void print()
const R b(20,52);
b.print();
//调用void print () const
return 0;
}
类里面的数据也可以被设置为常数据,特点是不能在任何函数体内赋值,只能通过初始化列表来获得初始值。
class A{
public:
A(int i);
void print();
private:
const int a;//常数据成员
static const int b;//静态常数据成员
};
//静态数据只能在类外说明和初始化
const int A::b=10;
//常数据成员只能通过初始化列表来获得初值
A::A(int i):a(i);
想向形参传递常对象实参,只能通过值传递或者常引用传递,相比而言使用常引用作为参数可以避免不必要的对象拷贝,提高性能并降低内存消耗。特别是对于较大的对象或类类型,使用常引用可以更高效地传递参数。
利用常引用传参的优点,只要函数不改变形参对象的值,不是常对象我们也可以用常引用来传递提高性能,但是考试的时候没有必要,简单易懂最好。
# include <iostream>
# include <cmath>
using namespace std;
class Point {// Point类定义
public: //外部接口
Point(int x=0, int y=0):x(x), y(y) {}
int getX(){return x;}
int getY(){return y;}
friend float dist(const Point &p1,const Point &p2);//友元函数声明
private://私有数据成员
int x, y;
};
float dist(const Point &p1,const Point &p2){//友元函数实现
double x=p1.x-p2.x;//通过对象访问私有数据成员
double y=p1.y-p2.y;
return static_cast<float>(sqrt(x*x+y*y));
}
int main(){//主函数
const Point myp1(1,1), myp2(4,5);//定义 Point类的对象
cout<<"The distance is:";
cout<<dist(myp1, myp2)<<endl;//计算两点间的距离
return 0;
}
static
与类无关的static
只初始化一次,值可以被修改。
与类有关的static
在类中使用static可以解决类的不同对象间的共享问题。
静态成员是类的一部分,不属于任何单独的对象。所有类的对象共享同一静态成员。
静态成员函数可以操作静态数据成员,但非静态成员函数也可以操作静态数据成员。
静态成员函数可以通过类名直接调用,也可以通过对象来调用,但通常推荐使用类名直接调用。
静态数据成员必须在类外定义和初始化,且只能定义和初始化一次。
(因为静态数据成员不属于任何一个对象,需要专门分配一个类外空间)。
静态成员函数:静态成员函数不归任何对象管而是归类管,被类调用来访问静态成员(也可以经对象调用,但这样没意义,起作用的只是类的类型),但因为它不能区分对象,所以静态成员函数访问非静态成员时还是必须通过对象名。
下面是一个简单的代码示例:
#include <iostream>
class MyClass {
public:
static int staticData; // 静态数据成员
void nonStaticMemberFunction() {
std::cout << "Accessing static data member through non-static member function: " << staticData << std::endl;
}
static void staticMemberFunction() {
std::cout << "Accessing static data member through static member function: " << staticData << std::endl;
}
};
int MyClass::staticData = 10; // 静态数据成员的定义和初始化
int main() {
MyClass obj;
std::cout << "Accessing static data member through object: " << obj.staticData << std::endl;
obj.nonStaticMemberFunction();
MyClass::staticMemberFunction();
return 0;
}
六、指针和引用
指针
* 出现在声明语句,表示声明的是指针
int*p; // 声明p是一个 int 型指针
* 出现在执行语句,表示访问指针所指向对象的内容
cout << * P;//输出指针 p所指向的内容
对象指针
两种定义方法:
Test a(4); Test * p=&a ;在栈上分配,超出对象 a
的作用域时,它会自动被销毁,内存会被自动释放。
Test*q=new Test(4);在堆上使用 new
运算符动态分配内存,需要使用 delete
运算符手动释放内存。
new和delete
请注意,在使用 new
运算符动态申请内存后,需要使用相应的 delete
或 delete[]
运算符释放内存,以防止内存泄漏。
下面是使用 new
运算符动态申请一个数组、一个类对象和一个类对象数组的示例代码:
#include <iostream>
class MyClass {
public:
int num;
// 构造函数
MyClass(int n) : num(n) {
std::cout << "Constructor called for num = " << num << std::endl;
}
// 析构函数
~MyClass() {
std::cout << "Destructor called for num = " << num << std::endl;
}
};
int main() {
// 动态申请一个数组
int* arr = new int[5];
delete[] arr;
// 动态申请一个类对象
MyClass* obj = new MyClass(10);
delete obj;
// 动态申请一个类对象数组
MyClass* objArr = new MyClass[3] {MyClass(20), MyClass(30), MyClass(40)};
delete[] objArr;
return 0;
}
引用
&出现在变量声明语句中,表示声明的是引用,引用是变量的别名,即通过引用访问变量与通过变量名访问变量的效果是一样的。
声明引用时必须同时初始化,使它指向一个已存在的变量而且始终只作为这一个变量的别名。
引用不能指向常量。int &r=9;是错的。
int i,j;
int &ri=i;建立一个 int 型的引用ri,并将其初始化为变量i的一个别名
j=10;
ri=j;//相当于i=j;
&在执行语句中,表示取对象的地址,这时不叫引用而叫取地址符
int a,b;
int * pa,* pb=&b;
pa=&a;
引用的作用——引用传递修改实参
我们都知道函数调用时,给形参分配内存空间然后用实参来初始化形参,形参一旦获得值就与实参没半点关系了,所以修改形参时实参不会改变,这就是常说的值传递。
如何使在子函数中对形参做的修改对主函数中的实参有效呢?答案是引用传递。
因为一旦使用引用作为形参,函数调用时,给形参分配内存空间然后用实参来初始化形参,实际上就是声明了一个引用然后初始化,这样引用就始终指向了实参变量成为了实参的一个别名,对形参的任何操作也就会直接作用于实参。
七、运算符/表达式/变量/标识符
,和()
逗号运算符,将两个及其以上的式子联接起来,从左往右逐个计算表达式,整个表达式的值为最后一个表达式的值。括号运算符,先算括号里面的。
变量的生存期
生存期与程序的运行期相同的是全局变量和静态局部变量。全局变量在程序的任何地方都可以访问,并且其生存期是程序的整个运行期间。静态局部变量虽然在函数中声明,但其生存期也延长到程序的整个运行期间。
函数的形参和局部变量的生存期仅限于函数的执行期间
auto变量是默认的局部变量类型
变量的作用域
函数原型作用域:指在函数原型中声明的参数的作用域。这种作用域仅限于函数原型的参数列表中。
例如:
void foo(int x, int y); // x, y 在这里具有函数原型作用域
标识符
标识符以字母或下划线_开始,后面可以是字母、下划线、数字。
八、函数
函数的声明和定义
函数的原型声明,指在函数定义之前提前声明函数的名称、返回类型和参数列表。
原型声明时非必须的,而且形参名可以省略。
函数定义,指编写函数的具体实现代码。
函数定义不可以嵌套。
// 函数原型声明
int add(int, int);
// 函数定义
int add(int a, int b) {
return a + b;
}
函数参数赋初值
在C++中,当函数参数有默认值时,所有的默认参数都必须在所有的非默认参数之后。这意味着如果你为一个参数提供了默认值,那么它后面的所有参数也必须有默认值。所以,只有函数fun3的参数初始化是正确的:
int fun3(int a, int b, int c = 0){}
在这个函数中,参数c有一个默认值0,而参数a和b是非默认参数,它们都在有默认值的参数前面,满足C++的语法规则。
九、模版
模板在C++中主要有两种类型:函数模板和类模板。模板是一种在编译时进行参数化的多态性工具,可以用于生成具有不同类型的函数或类。
- 函数模板:定义的函数的参数类型和返回类型都是T。调用函数时函数名后<T的类型>。
#include<iostream>
using namespace std;
template <typename T>
T Max(T a, T b) {
return (a > b) ? a : b;
}
int main() {
cout << Max<int>(3, 7); // 输出7
cout << Max<double>(3.2, 7.4); // 输出7.4
return 0;
}
在上述代码中,Max
是一个函数模板,用于找出两个数中的较大者。T
是模板参数,可以是任何类型。在 main
函数中,我们通过在 Max
函数中明确提供模板参数 <int>
和 <double>
来调用函数模板。
- 类模板:类的成员数据和成员函数的参数类型和返回类型都是T。类初始化对象时类后<T的类型>。
#include<iostream>
using namespace std;
template <typename T>
class Box {
public:
Box(T x) : val(x) {}
T getVal() { return val; }
private:
T val;
};
int main() {
Box<int> intBox(3);
cout << intBox.getVal(); // 输出3
Box<double> doubleBox(3.14);
cout << doubleBox.getVal(); // 输出3.14
return 0;
}
在上述代码中,Box
是一个类模板,用于创建一个盒子,该盒子可以存储任何类型的值。T
是模板参数,可以是任何类型。在 main
函数中,我们通过在 Box
类中明确提供模板参数 <int>
和 <double>
来创建类模板的实例。
十、杂项
读程序写结果题中的递归
#include<iostream>
using namespace std;
long f(int n);
int main()
{
cout<<"main 函数运行"<<endl;
int i=5,num;
num= f(i);
cout<<i<<"!="<<num<<endl;
return 0;
}
long f(int n) //calculate n!
{
cout<<"进入n="<<n<<"时f()调用"<<endl;
int m;
if(n==1||n==0)
{
m=1;
cout<<"离开n="<<n<<"时f()调用,返回上一级"<<endl;
}
else
{
m=n*f(n-1);
cout<<"离开n="<<n<<"时f()调用,返回上一级"<<endl;
}
return m;
}
C++程序步骤
C++程序从上机到得到结果的一般操作步骤依次为编辑、编译、链接、运行。
预编译指令
预编译指令是在编译阶段处理的命令,主要用于告诉编译器在实际编译之前需要进行哪些预处理。以下是两个常见的C++预编译指令:
-
#include
:这个预编译指令用于包含其他文件。例如,如果你想使用C++的输入输出库,你需要在你的代码顶部包含这个库 -
#define
:这个预编译指令用于定义宏。例如,你可以定义一个宏来代表一个常数
这样,在你的代码中,每次出现宏在编译的时候都会被替换为常数。
异常
函数原型说明 int f(int, int) throw();
显示该函数不会抛出任何类型的异常。这是C++的异常规格说明符,其意味着函数承诺不抛出任何类型的异常。如果函数违反了这个承诺,程序会立即调用std::unexpected()
。
当然,这是一个C++代码示例,它全面展示了异常处理的用法,包括throw、try、catch和自定义异常类。
这个程序中的division
函数可能会抛出一个异常。在main
函数中,我们使用try/catch
块来捕获和处理可能的异常。如果y
为0,我们会抛出一个MyException
类型的异常,然后在catch
块中捕获和处理它。如果抛出的不是MyException
类型的异常,我们使用catch (...)
来捕获所有其他类型的异常。
#include <iostream>
#include <string>
// 自定义一个异常类
class MyException : public std::exception {
std::string message;
public:
MyException(const std::string& msg) : message(msg) {}
const char* what() const noexcept override {
return message.c_str();
}
};
// 定义一个函数,可能会抛出异常
double division(int a, int b) {
if (b == 0) {
throw MyException("Divisor cannot be zero!"); // 抛出一个自定义异常
}
return static_cast<double>(a) / b;
}
int main() {
int x = 10;
int y = 0;
double z = 0;
try {
z = division(x, y);
std::cout << "The result is: " << z << std::endl;
}
catch (const MyException& e) { // 捕获并处理异常
std::cerr << "Caught an exception: " << e.what() << std::endl;
}
catch (...) { // 捕获所有其他类型的异常
std::cerr << "Caught an unknown exception." << std::endl;
}
return 0;
}
改错题
new了最后一定要delete我总是忘了
/注释这个错很低级但容易漏了
写程序的一些习惯
运算符重载函数成员函数一定要利用this对象 / 都用常引用 / bool返回if else
一次初始化全部对象。
基类中虚函数的类型和参数表一定写成子类实现函数的样子
长度和钱用double
继承与派生的编程题的主函数先初始化派生类对象a、b,
用基类的指针p分别指向派生类对象a、b,测试重写的虚函数/基类的成员函数。
用派生类对象a、b,测试派生类独有的成员函数。