C++ 基本语法
1 类与对象
1) private protected public
private :仅对类内可见,派生类不可见,类外不可见
protected :对类内可见,对该类的派生类可见,但对类外不可见
public:类内、类外、派生类均可见
2)嵌套类
class A
{
class B //1.此时类B的作用域在类A的作用域内,内层类的成员对外层类的成员没有影响/关系,重名也没关系 2.内层类若在外层类为private时,定义一个公开的内层类对象作为外层类的成员,通过这个成员可以访问到内层类中的数据;内层类若在外层类为public时,外层类名:内层类名 对象名,可直接实例化一个内层类的对象,用于访问内层类中的数据
{
int x;
int y;
}
int a;
int b;
B c,d; //3.内层内对象作为成员:内层类对象作为外层类的成员时,并不分配内存空间,在外层类实例化时才分配空间
}
关于命名空间:
namespace teacher
{
class task
{
public:
int a;
int b;
};
};
namespace student
{
class task
{
public:
int a;
int b;
};
};
//若只使用teacher中的task类,可以使用using namespace teacher
int main(void)
{
teacher::task a;
student::task b;
a.a = 1;
a.b = 2;
b.a = 1;
b.b = 2;
return 0;
}
3)函数的重载与缺省
-
函数的重载:通过函数参数的类型的不同或参数数量的不同来判断重载,但不能通过返回值类型不同来判断函数的重载(因为调用函数时还不知道返回值的类型)
-
函数参数的缺省:参数的缺省必须从右向左缺省,且最好在函数声明处设置缺省参数(也可在函数定义处声明缺省参数,但不要同时进行)
eg:void set(int x,int y,int c = 10)
{…}
4)new与delelte
A* a = new A(1,2);delete a;//在堆空间上申请一个类对象a,并传递构造函数的参数,然后释放这个空间
int *b = new int[5];delete b[];//在堆空间上申请一个int数组b,然后释放这个空间
*:new出的对象存在于自由存储区,自由存储区时c++的抽象概念,编译器默认自由存储区在堆上,也可设置在栈上,new与malloc最大的不同是,实例化对象时malloc无法引起构造函数的调用
5)构造函数
-
构造函数名与类名相同:类名(参数…),没有返回值,可以有参数,可以重载
-
若不显示指定构造函数,编译器会默认提供一个构造函数,若显示指定构造函数了,编译器则不会指定默认的构造函数
-
在实例化对象时一定会调用构造函数,构造函数一般设置为public,若设置为protected或者private是无法直接实例化对象的
-
构造函数为private的应用:(单例模式,自己创造自己,)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QNL5DEIx-1631810485336)(C:\Users\Lution Young\AppData\Roaming\Typora\typora-user-images\image-20210623155910426.png)]
6)析构函数
- 析构函数名:~类名(),没有返回值,没有参数,因此析构函数只能有一个,不能形成重载
- 在有指针的类里可能要显示指定析构函数,不使用编译器默认提供的
- 析构函数在对象的生命周期结束时自动被调用,负责回收对象
*:先构造的后析构;在main函数执行之前会先初始化全局变量(即这些变量的构造函数会先于构造函数执行);static修饰的变量只初始化一次(构造函数只调用一次)
7)隐式构造
两种隐式构造:
-
不省略构造函数的形式
eg :初始化对象时:A a = A(1,2);//此时不产生临时对象
对象已经初始化后赋值时:a = A(1,2);//此时会产生一个临时对象,使用编译器默认提供的赋值运算符重载进行赋值,该临时对象在下一行就析构了。
-
省略构造函数的形式
只能用于初始化对象时,且构造函数可以只传递一个参数,构造函数没有使用explicit声明时
eg:A a = 10;//该情况也不产生临时对象
8)拷贝构造函数
1. 拷贝构造函数名与类名相同:参数一般为该类的引用 eg:A::A(A& a){...}
2. 编译器会默认提供拷贝构造,该函数会根据传进来的引用逐一给对应的成员赋值,但会造成浅拷贝导致重复析构,若成员有指针且在堆空间申请内存的应该自己提供拷贝构造
3. 拷贝构造也是构造函数,一般用于实例化对象时 eg: A a(10);A b(a);
*:1)引用在定义时就必须指出引用对象,且不能更换引用对象 (只能指定一次应用对象)
eg: A& a = b;a引用b,则a不能再引用其他对象了
2)引用实质是变量地址的别名,不占用内存空间,且不存在空指针、野指针的问题
3)使用引用传参数或者返回时,要注意引用的对象有没有被析构
4)构造函数无参实例化对象时,A a()会被编译器认为是声明一个函数,最好使用A a来代替。
5)若不想修改引用最好加入const修饰(如果一个函数的形参是引用,且要接收临时变量,这个引用前要加const)
2021/6/24
class A
{
...
public:
A(int x = 1,int y = 2,int z = 10){this->x = x;this->y = y;this->z = z;}
void show() const{cout << x << ' ' << y << ' ' << z << endl;}//该函数用const修饰了
};
void add(const A& a)//常引用作为形参,通过这个参数只能访问所引用对象中的常函数
{
a.show();
}
int main(void)
{
add(A(1,2,3));//隐式构造形成的临时对象作为实参传递时,形参必须加const,因为c++认为修改一个临时对象没有意义
return 0;
}
9)内联函数
作用:不需要让函数压栈出栈,直接将函数代码复制到调用处,运行速度更快
类外定义时加inline
class A
{
...
}
inline int A::Max(int x, int y)
{
return (x > y)? x : y;
}
类内定义的函数默认申请成为inline函数
10)this指针
- 成员函数调用时this指针回隐含的传入。
- this指针指向的就是引起该成员函数调用的对象
- this指针是个全局变量,且使用了const修饰因此不能手动改变 ,类似于A* const this
11)静态数据成员
静态成员也叫共享成员,所有对象都共享这个成员
-
如何定义一个静态成员
先定义一个类,在数据成员前加static声明。
再在类外对该静态成员进行定义性说明,格式为 成员数据类型 类名::成员名 (= 初值)
class A { int x; int y; static int z; public: A(int x = 1,int y = 2){this->x = x;this->y = y;} void show() const{cout << x << ' ' << y << ' ' << z << endl;} }; int A::z = 10;//定义性说明 可以赋初值也可以不不赋初值
-
静态成员的特点
对象之间是共享的,都可以修改该成员;(一个对象修改,其他所有对象的该成员都会同样修改);
可以直接借助于类拿到该成员,不占用对象的存储空间(属于类但不属于对象),因此与全局变量类似,全局都可以拿到该成员,但是受到类的访问权限限制;
存储在静态存储区;在使用时尽量不要在构造函数中进行赋值;
12)静态成员函数
1.如何成为静态成员函数
在类内函数声明时加入static修饰,但在函数定义出不能加入static修饰
2.静态成员函数的特点
可以借助于类直接调用(但同样受到类的访问权限的限制):类名::函数名(实参...);
静态成员函数没有this指针,因此函数内部不能访问非静态成员和非静态函数;
但可以将静态成员函数的参数设置为对象/对象的引用,这样就可以访问到类中的非静态数据成员和非静态成员函数;
class A
{
int x;
int y;
static int z;
public:
A(int x = 1,int y = 2){this->x = x;this->y = y;}
static void show(A& a) {cout << z << endl;cout <<a.x << ' ' << a.y << endl;}
};//可以将静态成员函数的参数设置为对象/对象的引用,访问到类中的非静态数据成员和非静态成员函数且没有限制(因为静态函数也是类的成员函数)
int A::z = 10;
int main(void)
{
A a(1,2);
A::show(a);//直接借助于类调用静态函数
return 0;
}
13)普通函数作为友元函数
-
如何定义一个友元函数
在类定义时,加入fiend关键字修饰,该函数就成为了该类的友元函数。
- 特点
友元函数对该类的成员访问无限制,该友元函数可以在类内定义也可以在类外定义,但注意无论如何,该友元函数不是类的成员函数,是个普通函数,不受类的访问权限限制。
因此为了访问到该类的成员,友元函数的参数一般是对象或者对象的引用。
注意同static一样,friend关键字不要在该函数定义时加。
友元函数会破坏类的封装性。
class A
{
int x;
int y;
static int z;
public:
A(int x = 1,int y = 2){this->x = x;this->y = y;}
friend void show(A& a);//友元函数的参数一般是对象或者对象的引用。
};
int A::z = 10;
void show(A& a)
{
cout << a.z << endl;
cout <<a.x << ' ' << a.y << endl;
}
int main(void)
{
A a(1,2);
show(a);
return 0;
}
14)成员函数作为友元函数
一个类的成员函数作为另外一个类的友元函数,同样也可以使用该成员函数无限制的访问另外一个类,但需要注意声明和定义顺序:
先声明friend关键字所在的类;
再定义非friend关键字所在的类;
再定义friend关键字所在的类;
最后定义各种成员函数;
class B;//先声明friend关键字所在的类;
class A //再定义非friend关键字所在的类
{
int x;
int y;
public:
void show(const B& a);
};
class B //再定义friend关键字所在的类;
{
int x;
int y;
public:
friend void A::show(const B&);
B(int a,int b):x(a),y(b){}
};
//最后定义各种成员函数;
inline void A::show(const B& a)
{
cout << a.x << ' '<< a.y << endl;
}
int main()
{
A a;
a.show(B(1,2));
return 0;
}
*:友元类:相当于对友元函数的扩展:
class A
{
...
}
class B
{
...
friend class A;
...
}
//此时类A中的所有成员函数都是类B的友元函数,但注意友元机制是单向的并且不可以继承
15)常成员
使用const修饰的成员为常成员,常成员在对象实例化后是无法再修改的,
因此必须提供初始化列表对常成员初始化,即类中有常成员时必须提供初始化列表。
16)常函数
-
如何成为常函数
类内声明函数时在函数名后边加const,函数定义时同样也要加上const,因为const可以形成函数的重载,且系统会优先调用没有const修饰的函数(常函数一般是给常对象使用的例如const A a;对象a就是一个常对象,常对象不能修改数据成员且只能调用常函数)。
-
常函数特点
常函数无法修改类中的数据成员,也无法调用非常函数(因为调用非常函数也会导致数据成员被修改);
2021/6/25
2 类的继承和派生
1)复写
派生类中的数据成员/成员函数若与基类中的数据成员/成员函数名称相同,则在派生类中是同时存在的,形成覆盖关系,且形成覆盖关系的函数在调用时会优先调用派生类中的函数
2)三种继承方式
public:公有派生,派生类中不改变基类中的成员访问权限
protected:保护派生,派生类中将基类中的成员的访问权限都改为protected
private:私有派生,派生类中将基类中的成员的访问权限都改为private
注意:基类中private的成员在派生类中无论何种派生方式都不可访问;基类中的构造函数和析构函数是无法继承的
-
一般使用public继承方式
-
构造函数访问权限protected时,为抽象类,无法实例化对象,只能派生
-
实例化派生类时,基类的构造函数也会被调用,因此需要在派生类的构造函数中使用初始化列表给基类的构造函数传参数(当基类的构造函数需要参数时)
-
构造与析构顺序:实例化派生类对象时会引起基类构造函数的调用,先构造基类,再构造对象成员,再构造派生类;派生类对象析构时会引起基类析构函数的调用,再析构基类(先析构派生类);派生类的析构函数负责新增成员的清理,继承下来的成员由基类析构函数清理,各负责各的。
(构造基类->构造对象成员->构造派生类对象)
*:若类中有对象成员,且对象成员的构造函数需要参数时,也需要在初始化列表中给这个对象成员传递构造函数的参数:
class Base
{
int x;
int y;
public:
Base(int x,int y):x(x),y(y){}
}
class Derive:public Base
{
int a;
int b;
Base c;//一个类对象作为类的成员(对象成员)
public:
Derive(int x,int y,int a,int b):Base(x,y),c(a,b),a(1),b(2){}//给基类构造函数传参数;给对象成员的构造函数传参数;让成员a、b初始化;并且成员的构造顺序和初始化列表没有关系,和类定义时的声明顺序有关。
}
~~~
3)多继承
多继承指一个派生类的直接基类由多个
需要注意的是: 在定义派生类时要每个基类前都要加继承方式;
这些基类的构造顺序取决于继承的顺序;
这些基类都需要传递构造函数的参数(都需要构造函数参数的情况下);
class Base1//直接基类1
{
int x;
int y;
public:
Base1(int x,int y):x(x),y(y){cout << 'A' << endl;}
};
class Base2//直接基类2
{
int a;
int b;
public:
Base2(int x,int y):a(x),b(y){cout << 'B' << endl;}
};
class Derive:public Base1,public Base2//派生类定义时,最好每个基类前都加继承方式
{
int m;
int n;
public:
Derive(int x,int y,int a,int b,int m,int n):Base1(x,y),Base2(a,b),m(m),n(n){cout << 'C' << endl;}//每个基类构造函数参数的的传递
};
*:多继承允许多个基类派生出一个派生类,不允许多次继承,即一个派生类多次继承同一个基类
eg:
class A:public Base1,private Base1 //不允许多次继承,此时A中会两个同名的基类成员,导致二意性,若确实有需要时,可在派生类中添加基类的对象作为成员
{
...
}
多继承也容易出现二义性问题,如Base 1中有x成员,Base 2中也有名为x的成员,此时派生类要访问到Base1中的x成员,使用Base1::x即可,如cout << a.Base1::x << endl;访问派生类对象a中的Base1中的x成员。
*:在继承时,若派生类新增的成员与基类成员同名时,这两个成员同时存在,形成覆盖关系,但系统会优先调用派生类新增的成员,此时若要想调用基类中的同名成员,需要指出是基类的同名成员,使用 基类名::成员指出
2020/6/28
4)赋值兼容规则
- 基类指针派 = 派生类对象的地址 (反之不行)
- 基类的引用 = 派生类对象
- 基类对象 = 派生类对象
5)多重继承
多重继承时,最终的派生类只能给直接基类的构造函数传递参数,不能给间接基类传递构造函数的参数
6)虚基类
菱形继承时,A派生B,A派生C,B与C再派生出D,这就导致D中出现两份A的拷贝,出现二义性问题
A
B C
D
解决方法:使用虚基类,使D中只有一份A的拷贝
使用步骤:在定义B和C时,使用virtual对A进行继承,如class B:public virtual A … ,注意在每个派生类/最终派生类中都要使用virtual关键字进行继承。
注意事项:虚基类的构造函数由最终的派生类传递,即最终派生类直接给虚基类传递构造函数,虚基类的成员的值只与最终派生类有关,其他类对虚基类传递的构造函数都无效但是必须写
eg:
class A
{
int x;
int y;
public:
A(int x,int y):x(x),y(y){}
void show();
};
void A::show()
{
cout << "A member value:" << x << ' ' << y << endl;
}
class B:public virtual A //虚基类继承A
{
int a;
int b;
public:
B(int a,int b,int c,int d):A(a,b),a(c),b(d){}
};
class C:public virtual A //虚基类继承A
{
int c;
int d;
public:
C(int a,int b,int c,int d):A(a,b),c(c),d(d){}
};
class D:public B,public C
{
int e;
int f;
public:
D(int a,int b,int c,int d,int e,int f):A(a,b),B(c,d,a,b),C(a,b,e,f),e(1),f(1){} //最终派生类要给虚基类传递构造函数的参数,虚基类的成员的值也由最终派生类传递的参数决定
};
int main(void)
{
D a(1,2,3,4,5,6);
a.show();
return 0;
}
3 多态
1)两种多态性
多态性指同一名称函数有多种实现(多种不同的代码)
-
静态多态性
在编译时就确定了走哪条路,函数重载、运算符重载均为静态多态性
-
动态多态性
在程序运行时才直到走哪条路,虚函数为动态多态性
2)运算符重载
不允许重载的五个运算符:
1. .
2. .*
3. ::
4. ?:
5. sizeof
注意运算符重载无法改变结合性、运算数目、优先级
-
成员函数做运算符重载
格式: operator运算符(形参)
特点:成员函数做运算符重载函数时,左操作数一般为类对象,才能引起运算符重载函数的调用,且左操作数(对象)会以this指针的形式传进去,右操作数以函数参数传进去。
注意:在例如连续加/减时会出现临时对象,如a+b+c时,a+b会先得到一个临时对象,这个临时对象再引起运算符重载函数调用与c操作再得到临时对象,
特例:对于++,–,前置与后置的形式不同
前置: <type> operator++(); 后置: <type> operator++(int);//注意后置++为先返回原值再++,因此重载时先记下原值,再自增,最后返回原值
class A { int x; int y; public: A(int x,int y):x(x),y(y){} A operator+(A& a); void show(){cout << x << ' '<< y <<' ' << endl;} A operator++(); A operator++(int); }; A A::operator+(A& a) { this->x += a.x; this->y += a.y; return *this; } A A::operator++() { ++x; ++y; return *this; } A A::operator++(int) { int a = x; int b = y; x++; y++; return A(a,b); } int main(void) { A a(1,2); A b(10,20); // A d(100,200); // A c = a+b+d; // c.show(); b = a++; a.show(); b.show(); return 0; }
*:对于 = 的赋值运算符重载,系统会默认提供一个,在必要时需要自己写如可能会出现浅拷贝情况时或需要实现连续赋值的情况(a=b=c=…)时,系统提供的默认赋值运算符重载函数的返回值类型为void。
-
友元函数做运算符重载函数
当左侧操作数不是对象时,可使用成员函数做运算符重载函数,此时所有的操作数均以函数参数的形式传递进去,没有this指针(此时运算符重载函数一般第一个参数为左操作数,第二个参数为右操作数)
格式:friend operator运算符(参数列表) 在类中要声明为友元函数
特例:前置++/–为friend operator++/–(操作数)
前置++/–为friend operator++/–(操作数,int)
注意: = ,() , [] , -> 只能用成员函数重载
class A { int x; int y; public: A(int x,int y):x(x),y(y){} friend A& operator++(A&); friend A operator++(A&,int); void show(){cout << x << ' ' << y <<endl;} }; A& operator++(A& a) { a.x++; a.y++; return a; } A operator++(A& a,int) { int c = a.x; int d = a.y; a.x++; a.y++; return A(c,d); } int main(void) { A a(1,2); A b(0,0); ++a; a.show(); b = a++; b.show(); a.show(); return 0; }
3)转换函数
转换函数是指将类对象转换为某个基本数据类型,转换函数必须是类的成员函数,没有返回值,但有return语句,没有参数
具体格式为:
类内声明为 operator 转换的类型(); 如 operator float()
类外定义:类名::operator 转换的类型(){…}
class A
{
int x;
int y;
public:
A(int x,int y):x(x),y(y){}
operator int();
};
A::operator int()
{
return x+y;//具体的转换算法
}
int main(void)
{
A a(1,2);
cout << a << endl;//此时会输出3;
return 0;
}
*:可使用友元函数对 << 运算符进行重载,此时就可以更改cout所要输出的数据了,一般可写为
friend ostream& operator<<(ostream&,className&) {/*输出算法*/}
在使用时,<<的重载函数若与转换函数同时存在时,系统会优先使用对<<的重载函数。
4)虚函数
通过虚函数实现了动态多态性。
虚函数的实现:在基类定义时在函数前加入virtual关键字,此时该函数成为虚函数,在基类的派生类中若增加一个同名的函数(覆写)该函数,则派生类中该函数也会成为虚函数。
通过虚函数实现动态多态性
1. 在基类声明一个虚函数
2. 派生类公有继承该基类,并增加一个与基类中虚函数同名的函数(覆写该函数),该函数也成为虚函数
3. 使用基类的指针/引用 接收 派生类对象地址/派生类对象
4. 再使用该基类的指针/引用调用这个虚函数,不会调用基类中的虚函数,而会调用实际传进来的对象中的虚函数(传哪个类对象,就调用哪个类对象中的函数)
原理:
类中存在虚函数时,类的开头会增加一个虚表指针,这个指针指向一张虚表,虚表中存放该类中的虚函数的地址,因此基类的指针/引用 接收 派生类对象地址/派生类对象时,使用基类的指针/引用调用虚函数,系统会先由虚表指针找到虚表,虚表中若有该函数,则调用虚表中的函数,而不是仅仅调用基类中的该函数。
虚函数实现了接口重用,使一个函数名字不变的情况下,实现了多种的功能(多种不同的代码)
class Point
{
int x;
int y;
public:
Point(int x,int y):x(x),y(y){}
virtual int area(){return 0;}//基类中声明一个虚函数
int getarea(){return 0;}
};
class Rect:public Point
{
int w;
int l;
public:
Rect(int x,int y):w(x),l(y),Point(0,0){}
int area(){return w*l;}//派生类中覆写该虚函数,该函数也自动成为虚函数
int getarea(){return w*l;}
};
int main(void)
{
Rect r(2,3);
Point& p = r;//使用基类引用接收派生类对象
cout << p.area() << endl;//通过基类引用调用该虚函数,会调用该基类引用实际指向的类对象中的虚函数
cout << p.getarea() << endl;//此函数未声明为虚函数,因此通过基类引用调用该函数时,仅仅只能调用引用的对象中基类部分的这个函数
return 0;
}
*:构造函数无法成为虚函数
对于析构函数,若基类将析构函数声明为虚函数,则所有派生类的析构函数均为虚函数,特别是在如下情况下析构函数必须申请为虚函数
Base* p = new Derive;//new了一个派生类对象赋值给基类指针
delete p;//此时在delete基类指针,且未声明基类析构函数为虚函数,仅仅会引起基类析构函数的调用,无法引起派生类析构函数的调用,若派生类数据成员中有指针申请的空间则无法被释放
/*这种情况必须将基类的析构函数声明为虚析构函数,delete基类指针时,基类析构函数与派生类析构函数均会被调用*/
即new了一个派生类对象赋值给基类指针时,且派生类成员有在堆上申请的有空间时,基类析构函数必须申请为虚函数。
若一个类不进行派生类,则没必要将析构函数必须申请为虚函数,这会加大内存的开销。
4)纯虚函数
纯虚函数的作用:包含纯虚函数的类为抽象类,无法实例化对象,只能用于派生,一般要在派生类中队纯虚函数进行覆写,若在派生类中没有对纯虚函数进行覆写,则会导致派生类也是抽象类。
格式:
virtual <type>函数名(参数列表) = 0;//在虚函数的声明后加"= 0"即为纯虚函数
注意纯虚函数不进行实现,仅需声明即可。
抽象类中的纯虚函数是提供给派生类的一个公共接口,可以用于在设计类时,某些函数暂时还无法实现,但必须要存在这样的函数,然后派生类覆写该纯虚函数即可使用该接口。
错误总结
-
使用虚函数实现多态时,虚函数的访问权限由基类决定(new派生类对象给基类指针时,基类指针调用该虚函数时,权限由派生类决定)
class Base { int a; int b; virtual void show();//基类虚函数为private public: Base(int a,int b):a(a),b(b){} }; void Base::show() { cout << "base" << a << ' ' << b << endl; } class Derive:public Base { int a; int b; public: Derive(int a,int b):Base(1,2),a(a),b(b){} void show();//派生类虚函数为public }; void Derive::show() { cout << "Derive" << a << ' ' << b << endl; } int main(void) { Base* p = new Derive(10,20);//new派生类对象给基类指针时 p->show();//此时会报错,无法访问到该虚函数 return 0; }
虚函数不进行实现,仅需声明即可。
抽象类中的纯虚函数是提供给派生类的一个公共接口,可以用于在设计类时,某些函数暂时还无法实现,但必须要存在这样的函数,然后派生类覆写该纯虚函数即可使用该接口。
错误总结
-
使用虚函数实现多态时,虚函数的访问权限由基类决定(new派生类对象给基类指针时,基类指针调用该虚函数时,权限由派生类决定)
class Base { int a; int b; virtual void show();//基类虚函数为private public: Base(int a,int b):a(a),b(b){} }; void Base::show() { cout << "base" << a << ' ' << b << endl; } class Derive:public Base { int a; int b; public: Derive(int a,int b):Base(1,2),a(a),b(b){} void show();//派生类虚函数为public }; void Derive::show() { cout << "Derive" << a << ' ' << b << endl; } int main(void) { Base* p = new Derive(10,20);//new派生类对象给基类指针时 p->show();//此时会报错,无法访问到该虚函数 return 0; }