教程链接:
C语言中文网 C++教程
第一章 从C到C++
case:
①学了C语言就相当于学了C++的一半,从C语言转向C++时,不需要从头开始,接着C语言往下学就可以。C++支持面向过程编程、面向对象编程和泛型编程,而C语言仅支持面向过程编程。就面向过程编程而言,C++和C语言几乎是一样。
②C++中的类可以看作C语言中结构体的升级版。C语言中的struct只能包含变量,而C++中的class除了可以包含变量,还可以包含函数。在C语言中,我们将它放在了struct 外面,它和成员变量是分离的;而在C++中,我们将它放在了class Student内部,使它和成员变量聚集在一起,看起来更像一个整体。
结构体和类都可以看作是一种由用户自己定义的复杂数据类型,在C语言中可通过结构体来定义变量,在C++中可以通过类名来定义变量。不同的是,通过结构体定义出来的变量还是叫变量,而通过类定义出来的变量有了新的名称,叫做对象。
③C++源文件的后缀在不同的编译器下有不同的后缀名。
推荐使用.cpp作为C++文件的后缀,更加通用和规范。
g++命令
gcc命令可以用来编译C++源文件
1.编译单个源文件:
gcc main.c -lstdc++
2.编译多个源文件
gcc mian.c second.c -lstdc++
GCC中还有一个g++命令,专门用来编译C++程序,广大C++开发人员也都使用这个命令。
1.编译单个文件
g++ main.cpp
2.编译多个文件
g++ mian.cpp module.cpp
3.使用-o选项指定可执行文件的名称:
./demo
④C++命名空间
命名空间主要是解决命名冲突。
ex:
namespace Li {
FILE fp = NULL;
}
namespace Han {
FILE fp = NULL;
}
namespace是C++中的关键字,用来顶一个命名空间。语法格式为:
namespace name {
//variables,functions,classes
}
name是命名空间的名字,它里面可以包含变量、函数、类、typedef、#define等,最后由{}包围.
使用变量、函数时要指明它们所在的命名空间。以最上面的fp变量为例。可以如此使用:
Han::fp = fopen("two.txt","rb+");
::称为域解析操作符,在C++中用来指明要使用的命名空间。除了直接使用域解析操作符,还可以采用using关键字声明。,ex:
fp = fopen("one.txt","r"); //使用Li定义的变量fp
Han::fp = fopen("two.txt","rb+");//使用Han定义的fp
在using声明后,如果有未具体制定命名空间的变量产生了命名冲突,那么默认采用命名空间Li中的变量。
完整的命名空间demo:
#include <iostream>
namespace Diy {
class Student {
public:
char *name;
int age;
float score;
public:
void say() {
printf("%s的年龄是%d,成绩是%f\n",name,age,score);
}
};
}
int main() {
std::cout << "Hello, World!s" << std::endl;
Diy::Student stu1;
stu1.name = "小明";
stu1.age =22;
stu1.score = 87;
stu1.say();
return 0;
}
C++头文件和std命名空间
C++之前用的都是C的库,后来引入了命名空间的概念,也就是std,std是standard的缩写,意思是标准命名空间。
1.旧的C++头文件,如iostream.h、fstream.h等将会继续被支持,尽管他们不在官方标准中。这些头文件的内容不在命名空间std中。
2.新的C++头文件,如iostream、fstream等包含的基本功能和对应的旧版头文件相似,但头文件的内容在命名空间std中。
3.标准C头文件如stdio.h、stdlib.h等继续被支持。头文件的内容不在std中。
4.具有C库功能的新C++头文件具有如cstdio、cstdlib这样的名字。他们提供的内容和相应的旧的C头文件相同,只是内容在std中。
对于不带.h的头文件,所有的符号都位于命名空间std中,使用时需要声明命名空间std;对于带.h的头文件,没有使用任何命名空间,所有符号都位于全局作用域。
But,对于原来C语言的头文件,即使按照C++的方式来使用,例如#include <cstdio>
这种形式,那么符号可以位于命名空间std中,也可以位于全局范围内,ex:
#include <cstdio>
void stdDemo(){
std::printf("Hello啊");
}
void cDemo(){
printf("你好啊~");
}
这都是能编译过的。
5.虽然C++几乎完全兼容C语言,C语言中的头文件在C++中依然被支持,但C++新增的库更加强大和灵活,我们尽量用C++新增的头文件,例如iostream、fstream、string等。所以以后尽量用C++的方式写代码吧。
ex:
void demoString() {
using namespace std;
string str;
int age;
cin>>str>>age;
cout<<str<<"已经成立"<<age<<"年了!"<<endl;
}
result:
c语文中文网
6
c语文中文网已经成立6年了!
这里我们只需要留意using namespace std;
它声明了命名空间std,后续如果有未指定命名空间的符号,那么默认使用std,代码的string、cin、count都位于命名空间std中。
Tips:在demoString中这个函数中声明了命名空间std,它的作用范围就只位于此函数当中,如果在其他函数中又用到了std,就需要重新声明!!如果希望在所有函数当中都是用命名空间std,可以将他声明在全局范围中~!
ex:
using namespace std;
将std直接声明在所有函数外部,虽然方便,但在中大型项目开发中是不被推荐的,这样做增加了命名冲突的风险,推荐在函数内声明std。(我觉的大家都喜欢方便..so,你懂得~)
C++输入输出
如图,C++中输出输出简单的demo。
①C语言中的输出输出库,scanf和printf仍然可以在C++中使用。
②在C++中要使用输出输出时,需要包含头文件iostream,其中包含了输出输出的对象,例如常见的cin表示输入,cout表示输出,cerr表示标准错误。
③cout和cin都是C++的内置对象,而不是关键字!cout和cin分别就是ostream和istream类的对象,这就是C++中的内置对象。
④使用cout进行输出需要紧跟<<运算符,使用cin需要紧跟>>运算符,这两个运算符可以自行分析所处理的数据类型,无需向scanf和printf那样给出格式控制字符串,不用%d,%f什么的了~
⑤endl表示换行,与c语言中的\n作用相同,也可以用\n来替代endl,也就可以写作:
cout<<"Please input an int number:\n"
,endl,end of line。⑥cin和cout都可以连续进行输出输出,比如:
cout和cin的用法非常强大灵活,上面只是最基本的功能,后面我们会详细介绍,它们比C语言中的scanf、printf更加灵活易用。
C++ 布尔类型(bool)
在c语言中并没有彻底从语法上支持“真”和“假”,只是用0和非0表示。C++中新增了bool类型,一般占用1个字节长度,两个取值,true & false,在cin/cout中输入输出还是数字0或1,但是代码中还是可以用true和false的。
常量 const关键字
在C语言中,const用来限制一个变量,表示这个变量不能改变,通常称这样的变量为常量(Constant)。在C++中const的含义并没有改变,只是对细节进行了一些调整。主要是以下两点:
①C++中的const更像是编译阶段的#define。
const int m = 10;
int n = m;
将m的值赋给n,这个赋值的过程在C和C++中是有区别的。
在C语言中,编译器会先到m所在的内存取出一份数据,再将这份数据赋值给n;而在C++中,编译器会直接将10赋值给m,没有读取内存的过程。C++对const的处理少了读取内存的过程,优点是提高了程序执行效率,缺点是不能反映内存的变化,一旦const变量被修改(通过指针还是可以修改的),C++就不能取得最新的值。
void demoConst() {
// const int m = 10;
// int n = m;
const int n = 10;
int *p = (int *) &n;
*p = 99;
printf("%d\n", n);
}
这段代码以C语言的方式编译,结果是99,以C++的方式编译结果是10,差异来自于C和C++对const的处理方式不同。
C语言中对const的处理和普通变量一样,会到内存中读取数据;C++对const的处理更像是编译时期的#define,是一个值替换的过程,不会读取内存。
②C++中全局const变量的可见范围是当前文件
普通全局变量的作用域是当前文件,但在其他文件中也是可见的,使用extern声明后就可以使用。下面这段代码在C和C++中都一样。
如果在变量前加上extern关键字,运行结果也一样。这说明在C语言中,const变量在多文件编程时的表现和普通变量一样,除了不能修改,没有其他区别。
但是如果在C++中加上const,代码就不能运行了,因为C++对const的特性做了调整,全局const变量的作用域仍然是当前文件,但是它在其他文件中是不可见的。,如下图;
总结:
C和C++中全局const变量的作用域相同,都是当前文件,不同的是它们的可见范围:C语言中const全局变量的可见范围是整个程序,在其他文件中使用extern声明后就可以使用;而C++中全局变量的可见范围仅限于当前文件,在其他文件中不可见,所以它可以定义在头文件(.h)中,多次引入后也不会出错。
new 和 delete运算符
在C语言中,动态分配内存用malloc()函数,释放内存用free()函数,如下所示:
void testMalloc() {
int *p = (int*) malloc(sizeof(int)*10);//分配10个int型内存空间
free(p);//释放内存
}
void testNewAndDelete() {
int *p = new int; //分配一个int型的内存空间
delete p; //释放内存
int *d = new int[10];//分配10个int型的内存空间
delete[] p;
}
在C++中,这两个关键字仍然能用,但是C++中又增加了两个关键字,new和delete,new用来动态分配内存,delete用来释放内存。
使用方式也很简单,上面对比一下就可以看出来。
new操作符会根据后面的数据类型来推断所需空间的大小。
为了避免内存泄漏,通常new和delete,new[]和delete[]操作符应该成对出现,并且不要和C语言中malloc()、free()一起混用。
C++ inline内联函数
函数是一个可以重复使用的代码块,CPU会一条一条挨着执行其中的代码。CPU再执行主调函数代码时如果遇到被调函数,主调函数就会暂停,CPU转而执行被调函数的代码,被调函数执行完毕后再返回到主调函数中,主调函数根据刚才的状态继续往下执行。执行过程可以认为是多个函数之间的相互调用的过程,它们形成了一个活简单或复杂的调用链条,这个链条的起点是main(),终点也是main()。当mian()调用完所有的函数,它会返回一个值来结束自己的生命,从而结束整个程序。
函数调用是有时间和空间开销的。如果函数体代码较多,需要较长的执行时间,那么函数调用机制占用的时间可以忽略;如果函数只有一两条语句,那么大部分的时间都会花费在函数调用机制上,这种时间开销就不容忽视。为了消除这种时间开销,在编译时将函数调用出用函数体替换,类似于C语言中的宏展开。这种在函数调用处直接嵌入函数体的函数被称为【内联函数】(Inline Function),又称内嵌函数或者内置函数。
指定内联函数的方法很简单,只需要在函数定义处增加inline关键字,如下:
注意,要在函数定义处添加inline关键字,在函数声明处添加inline关键字虽然没错,但是是无效的,编译器会忽略函数声明处的inline关键字。
当编译器遇到函数调用swap(&m,&n)时,会用swap()函数的代码替换swap(&m,&n),同时用实参代替形参,
swap(&m,&n)
被置换成:
temp = *(&m);
*(&m) = *(&n);
*(&n) = temp;
由于内联函数比较短小,通常做法是省略函数原型,也就是函数声明,将整个函数定义放在本应该提供函数原型的地方(也就是将函数直接定义在main函数上面)。
函数内联函数的缺点也非常明显,编译后的程序会存在多份相同的函数拷贝,如果被声明为内联函数的函数体非常大,那么编译后的程序体积也会变的很大,所以必须牢记:一般只将那些短小的、频繁调用的函数声明为内联函数~
inline声明只是程序员对编译器提出的一个建议,并不是强制性的,编译器有自己的判断能力,他会根据具体情况决定是否这么做。
使用内联函数替代宏
//TODO
如何规范使用C++内联函数
inline关键字可以只在函数定义处添加,也可以只在函数声明处添加;但是在函数声明处添加的inline关键字是无效的,编译器会忽略掉。
内联函数不应该有声明,应该将函数定义在本应该出现函数声明的地方,这是一种良好的编程风格。
函数默认参数及使用场景
C++支持默认参数,默认参数只能放在形参列表的最后,而且一旦为某个形参指定了默认值,那么它后面的所有形参都必须有默认值。(这一点看起来还是kt默认值更方便点,没这个强制)
ex:
void demoDefaultValue(int a,float b= 1.0,bool c = false){
cout<<"a,b,c value = "<<a<<b<<c;
}
可以在函数定义处声明默认值,也可以在函数声明处声明默认值,但是有一点必须记住:在给定的作用域中只能指定一次默认参数。
如图:
如果把声明放在其他文件中就可以了:
在多文件编程时,我们通常的做法是将函数声明放在头文件中,并且一个函数只声明一次,但是多次声明同一个函数也是合法的。
有一点需要注意,在给定的作用域中一个形参只能被赋予一次默认参数,函数的后续声明只能为之前那些没有默认值的形参添加默认值,并且该形参右侧的所有形参必须都有默认值。
这种声明是正确的,第一次给c指定了默认值,第二次给b指定了默认值;但第二次不能再次给c指定默认参数,否则就是重复声明同一个默认参数。
C++支持函数重载
C++支持函数重载,这一点跟java差不多,参数的类型、参数的个数和参数的顺序,只要有一个不同即可。
C++函数重载过程中的二义性和类型转换
当实参的类型和形参的类型不一致时情况就会变得稍微复杂,例如函数形参的类型是int,调用函数时却将short类型的数据交给了它,编译器就需要先将short类型转换为int类型才能匹配成功。
C++标准规定,在进行重载决议时编译器应该按照下面的优先级顺序来处理实参的类型;
编译器按照从高到低的顺序来搜索重载函数,首先是精确匹配,然后是类型提醒,最后才是类型转换,一旦在某个优先级中找到唯一的一个重载函数就匹配成功,不再继续往下搜索。
如果在一个优先级中找到多个合适的重载函数,编译器就会陷入两难境地,不知道如何抉择就视为一种错误,这就是函数重载过程中的二义性错误。
比如上面去掉func(int)的定义去调用func(49),三个函数都能匹配上,那么就会发生二义性错误,编译器报错。
注意,类型提升和类型转换不是一码事,类型提升是积极的,是为了更加高效地利用计算机硬件,不会导致数据丢失或精度降低;而类型转换是不得已而为之,不能保证数据的正确性,也不能保证应有的精度。
C++和C的混合编程
C和C++进行混合编程时,考虑到对函数名的处理方式不同,势必会造成编译器在程序链接阶段无法找到函数具体的实现,导致链接失败。
这时候我们就需要借助【extern "C"】,解决C++和C在处理代码方式上的差异性。
extern是C和C++的一个关键字,但对于extern "C",我们需要视为一个整体,与extern毫无关系, extern "C" 既可以修饰一句C++代码,也可以修饰一段C++代码,它的功能是让编译器以处理C语言代码的方式来处理修饰的C++代码。
用extern "C" 之前:
在头文件中使用 extern "C"
//第一种方式
extern "C" void display();
void display();
//第二种方式,实际开发中可以使用
extern "C" {
void display();
}
这样在C和C++文件中使用这个头文件的时候,头文件的内容会被分别复制到这2个源文件中。编译器根据不同的源文件分别进行不同的处理。
类和对象
C++类的定义和对象的创建
C++中类和结构体一样,只是一种复杂数据类型的声明,不占用内存空间。而对象是类这种数据类型的一个变量,或者说是通过类这种数据类型创建出来的一份实实在在的数据,所以占用内存空间。
类是用户自定义的类型,C++本身并不提供现成的类的名称、结构和内容。
简单的类定义:
class Student {
public:
char *name;
int age;
float score;
void say() {
cout << name << age << score << endl;
}
};
`class`是C++新增的关键字,专门用来定义类。`Student`是类的名称;类的首字母一般大写。
`public`是C++新增的关键字,只能用在类的定义中,表示的意思和其他语言一样:成员变量或成员函数具有"公开"的访问权限。
类只是一个模板(Template),该数据类型的名称是Student。与char、int、float等基本数据类型不同的是,Student是一种复杂数据类型,可以包含基本类型,还有很多基本类型中没有的特性。
**创建对象**
`Student stu;//创建对象`
类名前可以加`class`关键字,不过我们一般都是省略掉。
也可以创建对象数组:
`Student stuArr[100];`
**访问类的成员**
创建对象后可以使用点号`.`来访问成员变量和成员函数。如下所示 :
void test1() {
// class Student stu; //class 可省略
Student stu;//创建对象
Student stuArr[100]; //创建对象数组
stu.name = "KuGou~";
stu.age = 21;
stu.score = 20.5;
stu.say();
}
使用对象指针
C语言中经典的指针在C++中仍然广泛使用,尤其是指向对象的指针,没有它就不能实现某些功能。
上面创建对象的方式:
Student stu;
会在栈上分配内存,通过地址符&
即可获取其地址。
还有一种可以在堆上分配内存,即使用new
关键字创建。但其与栈上创建出来的有点不同的是,栈上的有名字,不必非得指定指针指向它。但是【在堆上分配的是匿名的,没有名字,只能得到一个指向他的指针,所以必须使用一个指针变量来接收这个指针,否则以后再也无法找到这个对象了,更没有办法使用它】。
栈内存是程序自动管理的,不能使用使用delete删除在栈上创建的对象;
堆内存由程序员管理,对象使用完毕后可以通过delete删除。
在实际开发中,new和delete往往成对出现,以保证及时删除不再使用的对象,防止无用内存堆积。
堆上分配内存的:
void test1() {
// class Student stu; //class 可省略
Student stu;//创建对象
Student stuArr[100]; //创建对象数组
//在栈上分配内存
Student *pStu = &stu;
//在堆上分配内存
Student *pStu2 = new Student;
// stu.name = "KuGou~";
// stu.age = 21;
// stu.score = 20.5;
// stu.say();
pStu2->name = "SingleDog";
pStu2->age = 27;
pStu2->score = 20.5;
pStu2->say();
delete pStu2;//删除对象
}
#### C++类的成员变量和成员函数详解
类的成员变量和普通变量一样,也有数据类型和名称,占用固定长度的内存。但是,在【定义类的时候不能对成员变量赋值】(这点和Java不同),因为类只是一种数据类型或者说是一种模板,本身不占用内存空间,而变量的值则需要内存来存储。
类的成员函数也和普通函数一样,都有返回值和参数列表,与一般函数的区别在于:
成员函数是一个类的成员,出现在类体中,它的作用范围由类来决定;
普通函数是独立的,作用范围是全局的,或位于某个命名空间内。
**我们可以在类中声明函数,在类体外定义。但是成员函数必须先在类体中作原型声明,然后再类外定义,也就是说类体的位置应在函数定义之前。**
class Book {
public:
char *name;
char *author;
float price;
void logInfo();
};
void Book::logInfo() {
cout << name << "的作者是:" << author << ",价格是:" << price << endl;
}
void test2() {
Book *book_fp = new Book;
book_fp->name = "《麦田守望者》";
book_fp->author = "杰罗姆";
book_fp->price = 49.5;
book_fp->logInfo();
}
```
`::`被称为域解析符,用来连接类名和函数名,指明当前函数属于哪个类。
**在类体中和类体外定义成员函数的区别**
在类体中定义的成员函数会自动成为内联函数,在类体外定义的不会。可以在类体外定义的函数前加`inline`关键字使其与函数体内定义的相同。
内联函数一般不是我们期望的,它会将函数调用处用函数体替代,所以最好在类体内部对成员函数作声明,在类体外部进行定义,实际开发中大家都这么做的。
C++类成员的访问权限以及类的封装
C++通过public、protected、private三个关键字来控制成员变量和成员函数的访问权限,他们分别表示公有的、受保护的、私有的。
Tips:
Java和C#程序员需要注意,C++中的public、private、protected只能修饰类的成员,不能修饰类,C++中的类没有公有私有之说。
在类的内部,无论成员是被声明为public、protected还是private都能互相访问,没有访问权限的限制。
在类的外部,只能通过对象访问成员,并且通过对象只能访问public属性的成员,不能访问private、protected属性的成员。
下面来实际使用一下这几个限定符:
class Person {
private:
char *m_name;
int m_age;
int m_score;
public:
void setName(char *name);
void setAge(int age);
void setScore(float score);
void show();
};
void Person::setName(char *name) {
m_name = name;
}
void Person::setAge(int age) {
m_age = age;
}
void Person::setScore(float score) {
m_score = score;
}
void Person::show() {
cout << m_name << "的年龄是:" << m_age << ",成绩是:" << m_score << endl;
}
void test3() {
Person *person = new Person;
person->setName("二狗");
person->setAge(22);
person->setScore(87.5);
person->show();
}
成员变量大都以m_开头,这是约定俗称的写法。以m_开头一眼就可以看出这是个成员变量,又可以和成员函数中的形参名字区分开。(跟java的成员变量m开头类似。)
C++对象的内存模型
编译器会将成员变量和成员函数分开存储:【分别为每个对象的成员变量分配内存,但是所有对象都共享同一段函数代码。】如下图:
类可以看做是一种复杂的数据类型,可以用sizeof求得该类型的大小。在计算其类型大小时,只计算了成员变量的大小,并没有把成员函数也包含在内。
【对象的大小只受成员变量的影响,和成员函数没有关系。】
C++函数的编译
//TODO
C++构造函数
在C++中,有一种特殊的构造函数,它的名字和类名相同,没有返回值,不需要用户显式调用(用户也不能调用,是的,跟java一样,在创建对象的时候就直接调用了),而是在创建对象时自动执行。这种特殊的成员函数就是构造函数。
ex:
跟Java区别可能就在于:
①需要在public中声明构造函数
②构造函数是在类的外部定义
③声明和定义都不能出现返回值类型,void也不行
④函数体中不能有return语句
其他用法都类似。
构造函数必须是public属性的,否则创建对象时无法调用。设置为private、protected属性也不会报错,但是没有意义。
构造函数的重载
构造函数的调用是强制的,一旦在类中定义了构造函数,那么创建对象时就一定要调用,不调用是错误的。如果有多个重载的构造函数,那么创建对象时提供的实参必须和其中的一个构造函数匹配;反过来时,创建对象时只有一个构造函数会被调用。(这倒是和Java一样)
默认构造函数
一个类必须有构造函数,要么用户自己定义,要么编译器自动生成。一旦用户自己定义了构造函数,不管有几个,也不管形参如何,编译器都不再自动生成。
调用没有参数的构造函数可以省略括号,之前的demo中也应用过这一点。
C++构造函数初始化列表
构造函数的一项重要功能是对成员变量进行初始化,为了达到这个目的,可以在构造函数的函数体中对变量一一赋值,还可以采用初始化列表。
ex:
Person::Person(char *name, int age, float score):m_name(name),m_age(age),m_score(score) {
}
使用构造函数初始化列表并没有效率上的优势,仅仅是书写方便,尤其是成员变量较多时,这种写法非常简单明了。
初始化const成员变量
构造函数初始化列表还有一个很重要的作用,就是初始化const成员变量。初始化const成员变量的唯一方法就是使用初始化列表。(C++类中的成员变量不能再类中直接赋值)
析构函数
创建对象时系统会自动调用构造函数进行初始化工作,同样,销毁对象时系统也会自动调用一个函数来进行清理工作,例如释放分配的内存、关闭打开的文件等,这个函数就是析构函数。
析构函数也是一种特殊的成员函数,没有返回值,不需要程序员显式调用,实际上也无法显式调用,而是在销毁对象时自动执行。构造函数的名字和类名相同,而析构函数的名字是在类名前加一个~
符号。
析构函数没有参数,不能被重载,因此一个类只能有一个析构函数。
ex:
class VLA {
public:
VLA(int len);
~VLA();
private:
int *at(int i);//获取第i个元素的指针
private:
const int m_len;
int *m_arr;//数组指针
int *m_p;//指向第i个元素的指针
};
VLA::VLA(int len) : m_len(len) {
if (len > 0) {
m_arr = new int[len];
} else {
m_arr = NULL;
}
}
//析构函数
VLA::~VLA() {
delete[] m_arr;
}
①析构函数在对象被销毁时调用,而对象的销毁时机与它所在的内存区域有关。
在所有函数之外创建的对象是全局对象,它和全局变量类似,位于内存分区中的全局数据区,程序在结束执行时会调用这些对象的析构函数。
②在函数内部创建的对象是局部对象,它和局部变量类似,位于栈区,函数执行结束时会调用这些对象的析构函数。
③new创建的对象位于堆区,通过delete删除时才会调用析构函数;如果没有
delete,析构函数就不会被执行。(所以,new和delete还是要成对出现啊!)
C++对象数组
数组中的每个元素都是对象。
C++成员对象和封闭类详解
一个类的成员变量如果是另一个类的对象,就称之为"成员变量"。包含成员对象的类叫封闭类。
生成封闭类对象的语句一定要让编译器能够弄明白其成员对象是如何初始化的,否则就会编译错误。
成员对象的消亡
封闭类对象生成时,先执行所有成员对象的构造函数,然后才执行封闭类自己的构造函数。
但当封闭类对象消亡时,先执行封闭类的析构函数,然后再执行成员对象的析构函数,成员对象析构函数的执行次序和构造函数的执行次序相反,先构造的后析构。
C++ this指针详解
this是C++的一个关键字,也是一个const指针,它指向当前对象,通过它可以访问当前对象的所有成员。
所谓当前对象,是指正在使用的对象,例如对于stu.show()
,stu就是当前对象,this是指向stu。
ex:
this只能用在类的内部,通过this可以访问类的所有成员,包括private/protected/public属性的。
this是一个指针,要用->
来访问成员变量或成员函数。
this实际上是成员函数的一个形参,在调用成员函数时将对象的地址作为实参传递给this。不过this这个形参是隐式的,它并不出现在代码中,而是在编译阶段由编译器默默地将它添加到形参列表中。
C++ static静态成员变量
静态成员变量是一种特殊的成员变量,它被关键字static
修饰。
static成员变量属于类,不属于某个具体的对象,即使创建多个对象,也只为这个静态变量分配一个内存。
static成员变量的内存既不是在声明类时分配,也不是在创建对象时分配,而是在类外初始化时分配。即没有在类外初始化的static的成员变量不能用。(经测试发现初始化不能放在方法中!只能在类外体进行!)
static成员变量不占用对象的内存,而是在所有对象之外开辟内存,即使不创建对象也可以访问。
static成员变量和普通static变量一样,都在内存分区中的全局数据分配内存,到程序结束时才释放。这意味着static成员变量不随对象的创建而分配内存,也不随对象的销毁而释放内存。而普通成员变量在对象创建时分配内存,在对象销毁时释放内存。
初始化时可以赋初值,也可以不赋值,如果不赋值,默认初始化为0.
C++ static静态成员函数详解
普通成员函数可以访问所有成员,静态成员函数只能访问静态成员。
编译器在编译一个普通成员函数时,会隐式地增加一个形参this,并把当前对象的地址赋值给this,所以普通成员函数只能在创建对象后通过对象来调用,因为它需要当前对象的地址。而静态成员函数可以通过类来直接调用,编译器不会为它增加形参this,它不需要当前对象的地址。
静态成员函数与普通成员函数的根本区别在于:普通成员函数有this指针,可以访问类中的任意成员;而静态成员函数没有this指针,只能访问静态成员。(包括静态成员变量和静态成员函数)
C++ const成员变量和成员函数
const成员变量的用法和普通const变量的用法相似,只需要在声明时加上const关键字。初始化const成员变量只有一种方法,就是通过构造函数的初始化列表,之前也说过了。
const成员函数
const成员函数可以使用类中的所有成员(看似和普通函数一样),但是不能修改他们的值(emotional damage?),这种措施主要还是为了保护数据而设置的。const成员函数也称为常成员函数。
通常我们将get函数设置为常成员函数,读取成员变量的函数的名字通常以get开头,后跟成员变量的名字,所以通常将他们称为get函数。
常成员函数需要在声明和定义的时候在函数和头部的结尾加上const关键字,ex:
这里需要注意的是,const是放在函数名后面的,跟java的不同。
需要强调的是,必须在成员函数的声明和定义处同时加上const关键字。
比如:
char *getName() const
和char *getname()
是两个不同的函数原型。不加const可就是另外一个函数了~
Tips:区分一下const的位置:
①函数开头的const用来修饰函数的返回值,表示返回值是const类型,也就是不能修改,例如const char * getName()
②函数头部的结尾加上const表示常成员函数,这种函数只能读取成员变量的值,而不能修改成员变量的值,例如char * getname() const
C++ const对象
const可以用来修饰对象,称为常对象。一旦将对象定义为常对象之后,就只能调用类的const成员(包括const成员变量和const成员函数)了。
定义常对象的语法和定义常量的语法类似:
const class object(params);
class const object(params)
也可以定义const指针:
const class *p = new class(params);
class const *p = new class(params);
C++友元函数和友元类
一个类中可以由public、protected、private三种属性的成员,通过对象可以访问public成员,只有本类中的函数可以访问本类的private成员。但有一种例外情况:友元。借助友元,可以使得其他类中的成员函数以及全局范围内的函数访问当前类的private成员。
在当前类以外定义的、不属于当前类的函数也可以在类中声明,但要在前面加friend关键字,这样就构成了友元函数。友元函数可以是不属于任何类的非成员函数,也可以是其他类的成员函数。
友元函数可以访问当前类中的所有成员,包括public、protected、private属性的。
ex:
友元函数不是类的成员函数,在友元函数中不能直接访问类的成员,必须要借助对象!
也可以在类中声明其他类的友元函数。
友元类
友元类中的所有成员函数都是另外一个类的友元函数。例如将类B声明为类A的友元类,那么类B中的所有成员函数都是类A的友元函数,可以访问类A的所有成员,包括public、protected、private属性的。
ex:
Tips:
①友元的关系是单向的而不是双向的。如果声明了类B是类A的友元类,不等同于类A是类B的友元类,类A中的成员函数不能访问类B中的private成员。
②友元的关系不能传递。如果类B是类A的友元类,类C是类B的友元类,不等于类C是类A的友元类。
除非有必要,一般不建议把整个类声明为友元类,而只将某些成员函数声明为友元函数,这样更安全一些。
C++ class和struct的区别
在C语言中,struct只能包含成员变量,不能包含成员函数。而在C++中,struct类似于class,既可以包含成员变量,又可以包含成员函数。
C++中的struct和class基本是通用的,唯有几个细节不同:
①使用class时,类中的成员默认都是private属性的;而使用struct时,结构体中的成员默认都是public属性的。
②class继承默认是private继承,而struct继承默认都是public继承
③class可以使用模板,而struct不能
C++ string详解
string类处理起字符串来会方便很多,完全可以替代C语言中的字符数组或字符串指针。
使用string类需要包含头文件<string>
,看一下string相关的方法:
void test6() {
string s1; //只定义未初始化,编译器会将默认值""赋值给s1
string s2 = "c plus plus";
string s3 = s2;
string s4(5, 's'); //由5个's'组成的字符串
int s4len = s4.length();
}
在实际编程中,有时候必须要使用C分割的字符串,比如打开文件时的路径,为此string类提供了一个转换函数c_str()
,该函数能将string字符串转换为C风格的字符串。
访问字符串中的字符
string字符串也可以像C风格的字符串一样按照下标来访问其中的每一个字符。string字符串的起始下标仍是从0开始。
for (int i = 0; i <s4len ; ++i) {
cout<<s4[i]<<" ";
}
字符串的拼接
有了string类,我们可以使用+
或+=
运算符来直接拼接字符串,就不需要使用C语言中的strcat()/strcpy()/malloc()等函数来拼接字符串了,也不用担心空间不够会溢出了。
string字符串的增删改查
①插入字符串:
insert()
函数可以在string字符串中指定的位置插入另一个字符串,它的一种原型为:
string& insert(size_t pos,const string& str);
pos表示要插入的位置,也就是下标;str表示要插入的字符串,可以是string字符串,也可以是C风格的字符串。
s2.insert(7,"amazing~");
②删除字符串
erase()
函数可以删除string中的一个字符串,它的原型为:
string& erase(size_t pos = 0, size_t len =npos);
pos表示要删除的子字符串的起始下标,len表示要删除子字符串的长度。如果不指明len的话,那么直接删除从pos到字符串结束处的所有字符。
③提取子字符串
substr()
函数用于从string字符串中提取字符串,它的原型为:
string substr(size_t pos = 0 , size_t len = npos) const;
pos为要提取的子字符串的起始下标,len为要提取的子字符串的长度。
④字符串查找
string类提供了几个与字符串查找有关的函数,如下:
1>find()函数
find函数用于在string字符串中查找子字符串出现的位置,其中两种原型为:
size_t find (const string& str, size_t pos = 0) const;
size_t find (const char* s, size_t pos = 0) const;
第一个参数为待查找的子字符串,可以是string字符串,也可以是C风格的字符串。第二个参数为开始查找的位置;如果不指名,则从第0个字符开始查找。
2>rfind()函数
find()函数是从第二个参数开始往后找,而rfind()函数则最多查找到第二个参数处,如果到第二个参数所指定的下标还没有找到子字符串,则返回一个无穷大值。
3>find_first_of()函数
find_first_of()函数用于查找子字符串和字符串共同具有的字符在字符串中首次出现的位置
ex:
void test6() {
string s1; //只定义未初始化,编译器会将默认值""赋值给s1
string s2 = "c plus plus";
string s3 = s2;
string s4(5, 's'); //由5个's'组成的字符串
int s4len = s4.length();
for (int i = 0; i <s4len ; ++i) {
cout<<s4[i]<<" ";
}
s2.insert(7,"amazing~");
//删除字符串
s2.erase(0,2);
int index = s2.find('p',3);
int index2 = s2.find_first_of('s',5);
int index3 = s2.rfind('s',10);
}
本章小结
里面的内容在上面都有提到,贴个链接吧:
C++类和对象的总结
C++引用
C++引用入门
参数的传递本质上是一次赋值的过程,赋值就是对内存进行拷贝。所谓内存拷贝,是指将一块内存上的数据复制到另一块内存上。
对于char、bool、int等基本类型的数据,他们占用的字节往往只有几个字节,内存拷贝十分迅速。而数组、结构体、对象是一系列数据的集合,数据的数量没有限制,可能很少,也可能成千上万,对它们进行频繁的内存拷贝可能消耗很多时间,拖慢程序的执行效率。
所以C/C++禁止在函数调用时直接传递数组的内容,而是【强制传递数组指针】。而对于结构体和对象没有这种限制,调用函数时既可以传递指针,也可以直接传递内容;
在C++中,有一种比指针更加便捷的传递聚合类型数据的方式,那就是引用。
引用可以看作是数据的一个别名,通过这个别名和原来的名字都能够找到这份数据。类似Windows的快捷方式,一个可执行程序可以有多个快捷方式,所以一个数据也可以有多个别名。
引用的定义类似于指针,只是用&
取代了*
,语法格式为:
type &name = data;
Type是被引用的数据的类型,name是引用的名称,data是被引用的数据。引用必须在定义的同时初始化,并且以后也要从一而终,不能再引用其他数据,这有点类似于常量.
EX:
void testA() {
int a = 99;
int &r = a;
cout << "a value:" << a << endl;
cout << "r value:" << r << endl;
}
变量r就是变量a的引用,它们用来指代同一份数据;也可以说r是变量a的另一个名字。
注意,引用在定义时需要添加&,在使用时不能加&,因为使用时添加&表示取地址。
但有一个问题,这时候如果修改r的值,a的值也会随之改变,因为r和a都指向同一个地址。
如果不希望通过引用来修改原始的数据,可以在定义的时候添加const关键字,形式为:
const type &name = value;
也可以是:
type const &name = value;
C++引用作为函数参数
在定义或声明函数时,我们可以将函数的形参指定为引用的形式,这样在调用函数时就会将实参和形参绑定在一起,让它们都指代同一份数据。如此一来,如果在函数体中修改了形参的数据,那么实参的数据也会被修改,从而拥有”在函数内部影响函数外部数据”的效果。
Ex:
void swapA(int &r1, int &r2) {
int temp = r1;
r1 = r2;
r2 = temp;
}
void swapB(int *p1, int *p2) {
int temp = *p1;
*p1 = *p2;
*p2 = temp;
}
按引用传参在使用形式上比指针更加直观,鼓励使用,一般可以代替指针。
C++引用作为函数返回值
ex:
int &plusA(int &r) {
r += 10;
return r;
}
void testB() {
int a = 10;
int &r = a;
int b = plusA(r);
cout << "result:" << b << endl;
}
C++引用的本质
其实引用只是对指针进行了简单的分装,它的底层依然是通过指针实现的,引用占用的内存和指针占用的内存长度一样,在32位环境下是4个字节,在64位环境下是8个字节,之所以不能获取引用的地址,
是因为编译器进行了内存了内部转换。
C++的发明人Bjarne Stroustrup说过,他在C++中引入引用的直接目的就是为了让代码书写更漂亮,尤其是后面运算符重载。
引用和指针的区别
①引用必须在定义时初始化,并且以后也要从一而终,不能再指向其他数据;而指针没有这个限制,指针在定义时不必赋值,以后也能指向任意数据。
②可以有const指针,但是没有const引用。
③指针可以有多级,但是引用只能有一级,例如int **p
是合法的,而int &&r
是不合法的。
④指针和引用的自增(++)自减(--)运算意义不一样。对指针使用++表示指向下一份数据,对引用使用++表示它所指代的数据本身加1;自减(--)也是类似的道理。
C++引用不能绑定到临时数据
指针是数据货代吗在内存中的地址,指针变量指向的就是内存中的数据或代码。这里注意这个关键字,内存。指针只能指向内存,不能指向寄存器,因为寄存器和硬盘没法寻址。
C++代码中大部分内容都是放在内存中的,例如定义的变量、创建的对象、字符串常量、函数形参、函数体本身、new或malloc分配的内存,这些内容都可以用&
来获取地址,进而用指针指向他们。
但除此之外,表达式的结果、函数的返回值等,他们可能放在内存中,也可能会放在寄存器中。一旦被放到寄存器中,就没办法用&
获取他们的地址,也就没法用指针指向他们了。
继承与派生
继承可以理解为一个类从另一个类获取成员变量和成员函数的过程。例如类B继承于类A,那么B就拥有A的成员变量和成员函数。(这个和java没什么不同)
在C++中,派生和继承是一个概念,只是站的角度不同。继承是儿子接收父亲的产业,派生是父亲把产业传承给儿子。
被继承的类称为父类或基类,继承的类被称为子类或派生类。子类和父类通常放在一起称呼,基类和派生类通常放在一起称呼。(和Java类似啦)
#include <iostream>
using namespace std;
class People {
public:
void setname(char *name);
void setage(int age);
char *getname();
int getage();
private :
char *m_name;
int m_age;
};
void People::setname(char *name) {
m_name = name;
}
void People::setage(int age) {
m_age = age;
}
char *People::getname() {
return m_name;
}
int People::getage() {
return m_age;
}
class Student : public People {
public:
void setscore(float score);
float getscore();
private:
float m_score;
};
void Student::setscore(float score) {
m_score = score;
}
float Student::getscore() {
return m_score;
}
void testX() {
Student stu;
stu.setname("小明");
stu.setage(22);
stu.setscore(99.5);
cout << stu.getname() << "的年龄是:" << stu.getage() << ",成绩是:" << stu.getscore() << endl;
}
int main() {
testX();
return 1;
}
这里继承可能跟java不同的点在于继承的时候,父类名前也要加public,表示共有继承,继承方式包括public、private和protected,此项可选,默认为private。
继承的一般语法为:
class 派生类名:[继承方式] 基类名{
派生类新增加的成员
};
C++三种继承方式
继承方式限定了基类成员在派生类中的访问权限,包括public、private和protected。
protected成员和private成员类似,也不能通过对象访问。但是当存在继承关系时,protected和private就不一样了:基类中的protected成员可以在派生类中使用,而基类中的private成员不能在派生类中使用。
不同的继承方式会影响基类成员在派生类中的访问权限。
1.public继承方式
基类中所有public成员在派生类中为public属性
基类中所有protected成员在派生类中为protected属性;
基类中所有private成员在派生类中不能使用;
2.protected继承方式
基类中的所有public成员在派生类中为protected属性;
基类中的所有protected成员在派生类中为protected属性;
基类中的所有private成员在派生类中不能使用;
3.private继承方式
基类中的所有public成员在派生类中均为private属性;
基类中的所有protected成员在派生类中均为private属性;
基类中的所有private成员在派生类中不能使用。
基类成员在派生类中的访问权限不得高于继承方式中指定的权限。例如继承方式为protected时,那么基类成员在派生类中的访问权限最高为protected,高于protected的会降级为protected,但低于protected不会升级。
也就是说,继承方式中的public、protected、private是用来指明基类成员在派生类中的最高访问权限的。
在派生类中访问基类private成员的唯一方法就是借助基类的非private成员函数,如果基类没有非private成员函数,那么该成员在派生类中将无法访问。
改变访问权限
使用using关键字可以改变基类成员在派生类中的访问权限,例如将public改为private、将protected改为public。
Tips:using只能改变基类中public和protected成员的访问权限,不能改变private成员的访问权限,因为基类中private成员在派生类中是不可见的,根本不能使用,所以基类中的private成员在派生类中无论如何都不能访问。
#include <iostream>
using namespace std;
class People {
public:
void show();
protected:
char *m_name;
int m_age;
};
void People::show() {
cout<<m_name<<"的年龄是:"<<m_age<<endl;
}
class Student : public People {
public:
void learning();
public:
using People::m_name;//将protected改为public
using People::m_age; //将protected改为public
float m_score;
private:
using People::show; //将public改为private
};
void Student::learning() {
cout<<"我是:"<<m_name<<",今年"<<m_age<<"岁,这次考了"<<m_score<<endl;
}
void testX() {
Student stu;
stu.m_name = "小明";
stu.m_age = 16;
stu.m_score = 99.5f;
//stu.show();//compile error
stu.learning();
}
int main() {
testX();
return 1;
}
C++继承时的名字遮蔽问题
如果派生类中的成员和基类中的成员重名,那么就会遮蔽从基类继承过来的成员。
所谓遮蔽,就是在派生类中使用该成员时,实际上使用的是派生类新增的成员,而不是从基类继承来的。
但是基类中的函数仍然可以调用,不过要加上类名和域解析符。ex:
//使用的是从基类继承来的成员函数
stu.People::show();
基类成员函数和派生类成员函数不构成重载
基类成员和派生类成员的名字一样时会造成遮蔽,这句话对于成员变量很好理解,对于成员函数要引起注意,不管函数的参数如何,只要名字一样就会造成遮蔽。焕换句话说,基类成员函数和派生类成员函数不会构成重载,如果派生类有同名函数,那么就会遮蔽基类中的所有同名函数,不管它们的参数是否一样。
C++类继承时的作用域嵌套
//TODO
C++继承时的对象内存模型
//TODO
C++基类和派生类的构造函数
之前说过基类的成员函数可以被继承,可以通过派生类的对象访问,但这仅仅指的是普通的成员函数,类的构造函数不能被继承。构造函数不能被继承是有道理的,因为即使被继承了,它的名字和派生类的名字也不一样,不能成为派生类的构造函数,当然更不能成为普通的成员函数。(和java不同)
在设计派生类时,对继承过来的成员变量的初始化工作也要由派生类的构造函数完成,但是大部分基类都有private的成员变量,他们在派生类中无法访问,更不能使用派生类的构造函数来初始化。
解决这个问题的思路是:
在派生类的构造函数中调用基类的构造函数。
EX:
class Human{
protected:
char *m_name;
int m_age;
public:
Human(char *,int);
};
Human::Human(char * name, int age):m_name(name),m_age(age) {}
class Child:public Human{
private:
float m_score;
public:
Child(char*,int ,float);
void display();
};
Child::Child(char *name,int age,float score):Human(name,age),m_score(score){
}
void Child::display() {
cout<<m_name<<"的年龄是:"<<m_age<<",其成绩为:"<<m_score<<endl;
}
void testY(){
Child child("二狗",12,87.5);
child.display();
}
构造函数的调用顺序
基类构造函数总是被优先调用,这说明在创建派生类对象时,会先调用基类构造函数,再调用派生类构造函数,派生类构造函数中只能调用直接基类的构造函数,不能调用间接基类的。
C++基类和派生类的析构函数
和构造函数一样,析构函数也不能被继承。与构造函数不同的是,在派生类的析构函数中不用显式地调用基类的析构函数,因为每个类只有一个析构函数,编译器知道如何选择,无须程序员干涉。
另外析构函数的执行顺序和构造函数的执行顺序也刚好相反:
1.创建派生类对象时,析构函数的执行顺序和继承顺序相同,即先执行基类构造函数,再执行派生类构造函数。
2.而销毁派生类对象时,析构函数的执行顺序和继承顺序相反,即先执行派生类析构函数,再执行基类析构函数。
ex:
class A{
public:
A(){cout<<"A constructor"<<endl;}
~A(){cout<<"A destructor"<<endl;}
};
class B: public A{
public:
B(){cout<<"B constructor"<<endl;}
~B(){cout<<"B destructor"<<endl;}
};
class C: public B{
public:
C(){cout<<"C constructor"<<endl;}
~C(){cout<<"C destructor"<<endl;}
};
void testZ(){
C test;
}
输出:
A constructor
B constructor
C constructor
C destructor
B destructor
A destructor
C++多继承详解
之前派生类都只有一个基类,称为单继承。除此之外,C++也支持多继承,一个派生类可以有两个或多个基类。
(但是多继承容日让代码逻辑复杂、思路混乱,中小型项目中较少使用,后来的java、c#、PHP取消了多继承。)
多继承的语法比较简单,将多个基类用逗号隔开即可。例如声明了类A、类B和类C,那么可以用下面方式声明派生类D:
class D:public A,private B,private C{
}
多继承形式下的构造函数和单继承形式基本相同,只是要在派生类的构造函数中调用多个基类的构造函数。如下:
D(形参列表):A(实参列表),B(实参列表),C(实参列表){
}
基类构造函数的调用顺序和他们在派生类构造函数中出现的顺序无关,而是和声明派生类时基类出现的顺序相同。
ex:
class BaseA {
public:
BaseA(int a, int b);
~BaseA();
private:
int m_a;
int m_b;
};
class BaseB {
public:
BaseB(int c, int d);
~BaseB();
private:
int m_c;
int m_d;
};
class BaseC {
public :
BaseC(int a, int d);
~BaseC();
private:
int m_a;
int m_d;
};
class Deliver : public BaseC, public BaseA, public BaseB {
public:
Deliver(int a, int b, int c, int d, int e);
~Deliver();
private:
int m_e;
};
BaseA::BaseA(int a, int b) : m_a(a), m_b(b) {
cout << "BaseA constructor" << endl;
}
BaseA::~BaseA() {
cout << "BaseA destructor" << endl;
}
BaseB::BaseB(int c, int d) : m_c(c), m_d(d) {
cout << "BaseB constructor" << endl;
}
BaseB::~BaseB() {
cout << "BaseB destructor" << endl;
}
BaseC::BaseC(int a, int d) : m_a(a), m_d(d) {
cout << "BaseC constructor" << endl;
}
BaseC::~BaseC() {
cout << "BaseC destructor" << endl;
}
Deliver::Deliver(int a, int b, int c, int d, int e) : BaseC(a, d), BaseA(a, b), BaseB(c, d), m_e(e) {
cout << "Deliver constructor" << endl;
}
Deliver::~Deliver() {
cout << "Deliver destructor" << endl;
}
void testV() {
Deliver deliver(1, 2, 3, 4, 5);
}
int main() {
testV();
return 1;
}
C++虚继承和虚基类详解
多继承是指从多个直接基类中产生派生类的能力,多继承的派生类继承了所有父类的成员。但多个基类可能会带来错综复杂的问题,比如命名冲突。
C++虚继承和虚基类详解
正如文章所提到的,主要就是菱形继承。
比如A继承B,C,然后B,C又都继承D。这样B,C都有同样的从D中继承的成员变量,编译器就不知道选用哪个,就会产生歧义。
为了解决多继承时的命名冲突和冗余数据问题,C++提出了虚继承,使得在派生类中只保留一份间接基类的成员。
虚继承就是在继承方式前面加上virtual
关键字。
虚继承的目的是让某个类做出声明,承诺愿意共享它的基类。其中,这个被共享的基类就被称为虚基类。在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含一份虚基类的成员。
虚基类只影响从指定虚基类的派生类中进一步派生出来的类,它不会影响派生类本身。也就是不影响B,C,只影响D。
C++虚继承时的构造参数
在虚继承中,虚基类是由最终的派生类初始化的;换句话说,最终派生类的构造函数必须要调用虚基类的构造函数。对最终的派生类来说,虚基类是间接基类,而不是直接基类。这跟普通继承不同,在普通继承中,派生类构造函数中只能调用直接基类的构造函数,不能调用间接基类的。
#include <iostream>
using namespace std;
//虚基类A
class A{
public:
A(int a);
protected:
int m_a;
};
A::A(int a): m_a(a){ }
//直接派生类B
class B: virtual public A{
public:
B(int a, int b);
public:
void display();
protected:
int m_b;
};
B::B(int a, int b): A(a), m_b(b){ }
void B::display(){
cout<<"m_a="<<m_a<<", m_b="<<m_b<<endl;
}
//直接派生类C
class C: virtual public A{
public:
C(int a, int c);
public:
void display();
protected:
int m_c;
};
C::C(int a, int c): A(a), m_c(c){ }
void C::display(){
cout<<"m_a="<<m_a<<", m_c="<<m_c<<endl;
}
//间接派生类D
class D: public B, public C{
public:
D(int a, int b, int c, int d);
public:
void display();
private:
int m_d;
};
D::D(int a, int b, int c, int d): A(a), B(90, b), C(100, c), m_d(d){ }
void D::display(){
cout<<"m_a="<<m_a<<", m_b="<<m_b<<", m_c="<<m_c<<", m_d="<<m_d<<endl;
}
int main(){
B b(10, 20);
b.display();
C c(30, 40);
c.display();
D d(50, 60, 70, 80);
d.display();
return 0;
}
在最终派生类 D 的构造函数中,除了调用 B 和 C 的构造函数,还调用了 A 的构造函数,这说明 D 不但要负责初始化直接基类 B 和 C,还要负责初始化间接基类 A。而在以往的普通继承中,派生类的构造函数只负责初始化它的直接基类,再由直接基类的构造函数初始化间接基类,用户尝试调用间接基类的构造函数将导致错误。
C++向上转型
在c++中经常会发生数据类型的转换,例如将int类型的数据赋值给float类型的变量时,编译器会把int类型的数据转换为float类型再赋值;反过来,float类型的数据在经过类型转换后也可以赋值给int类型的变量。
类其实也是一种数据类型,也可以发生数据类型转换,不过这种转换只有在基类和派生类之间才有意义,并且只能将派生类赋值给基类,包括将派生类对象赋值给基类对象、将派生类指针赋值给基类指针、将派生类引用赋值给基类引用,这在C++中被称为向上转型。相应的,将基类赋值给派生类称为向下转型。
向上转型十分安全,由编译器自动完成;向下转型有风险,需要程序员手动干预。
ex:
#include <iostream>
using namespace std;
//基类
class A{
public:
A(int a);
public:
void display();
public:
int m_a;
};
A::A(int a): m_a(a){ }
void A::display(){
cout<<"Class A: m_a="<<m_a<<endl;
}
//派生类
class B: public A{
public:
B(int a, int b);
public:
void display();
public:
int m_b;
};
B::B(int a, int b): A(a), m_b(b){ }
void B::display(){
cout<<"Class B: m_a="<<m_a<<", m_b="<<m_b<<endl;
}
int main(){
A a(10);
B b(66, 99);
//赋值前
a.display();
b.display();
cout<<"--------------"<<endl;
//赋值后
a = b;
a.display();
b.display();
return 0;
}
输出结果为:
Class A: m_a=10
Class B: m_a=66, m_b=99
Class A: m_a=66
Class B: m_a=66, m_b=99
赋值的本质是将现有的数据写入已分配好的内存中,对象的内存只包含了成员变量,所以对象之间的赋值时成员变量的赋值,成员函数不存在赋值问题。
这种转换关系是不可逆的,只能用派生类对象给基类对象赋值,而不能用基类对象给派生类赋值。理由很简单,基类不包含派生类的成员变量,无法对派生类的成员变量赋值。同理,同一基类的不同派生类对象之间也不能赋值。
将派生类指针赋值给基类指针
C++多态与虚函数
面向对象程序设计语言统一都有封装、继承和多态三种机制。
多态指的是同一名字的事物可以完成不同的功能。多态可以分为编译时多态和运行时多态。前者主要指函数的重载、对重载函数的调用,在编译时就能根据实参确定应该调用哪个函数,因此叫编译时多态;而后者则和继承、虚函数等概念有关。本章提及的多态都是运行时多态。
ex:
//
// Created by 18041 on 2022/3/31.
//
#include "chapter5.h"
#include <iostream>
using namespace std;
class People {
public:
People(char *name, int age);
void display();
protected:
char *m_name;
int m_age;
};
People::People(char *name, int age) : m_name(name), m_age(age) {}
void People::display() {
cout << m_name << "今年" << m_age << "岁了," << "是个无业游民" << endl;
}
class Teacher : public People {
public:
Teacher(char *name, int age, int salary);
void display();
protected:
int m_salary;
};
Teacher::Teacher(char *name, int age, int salary) : People(name, age), m_salary(salary) {
}
void Teacher::display() {
cout << m_name << "今年" << m_age << "岁了," << "是个教师,每月收入为:" << m_salary << endl;
}
void testAA() {
People *p = new People("王小二", 23);
p->display();
p = new Teacher("李二狗", 24, 6000);
p->display();
}
int main() {
testAA();
return 1;
}
输出结果:
王小二今年23岁了,是个无业游民
李二狗今年24岁了,是个无业游民
当基类指针p指向派生类Teacher对象时,虽然使用了Teacher的成员变量,但是却没有使用它的成员函数,导致结果不伦不类。
也就是说:通过基类指针只能访问派生类的成员变量,但是不能访问派生类的成员函数。
为了消除这种尴尬,让基类指针能够访问派生类的成员函数,C++增加了虚函数。只需要在函数声明前面增加virtual关键字。
ex:
class People {
public:
People(char *name, int age);
virtual void display();
protected:
char *m_name;
int m_age;
};
People::People(char *name, int age) : m_name(name), m_age(age) {}
void People::display() {
cout << m_name << "今年" << m_age << "岁了," << "是个无业游民" << endl;
}
class Teacher : public People {
public:
Teacher(char *name, int age, int salary);
virtual void display();
protected:
int m_salary;
};
输出:
王小二今年23岁了,是个无业游民
李二狗今年24岁了,是个教师,每月收入为:6000
注意基类和派生类的成员函数都需要添加virtual关键字。
有了虚函数,基类指针指向基类对象时就使用基类的成员,包括成员函数和成员变量,指向派生类对象时就使用派生类的成员。换言之,基类指针可以按照基类的方式来做事,也可以按照派生类的方式来做事,他有多种形态,或者说有多种表现形式,我们称这种现象为多态。
C++提供多态的目的是:
可以通过基类指针对所有派生类的成员变量和成员函数进行全方位的访问,尤其是成员函数。如果没有多态,只能访问成员变量。
通过引用来实现多态:
EX:
void testBB() {
People p("王小二", 23);
Teacher t("李二狗", 46, 8300);
People &rp = p;
People &rt = t;
rp.display();
rt.display();
}
不过引用不像指针灵活,指针可以随时改变指向,而引用只能指代固定的对象,在多态性方面缺乏表现力。
C++虚函数注意事项以及构成多态的条件
C++虚函数对于多态具有决定性的作用,有虚函数才能构成多态。
说一下虚函数的构成条件:
1.只需要在虚函数的声明处加上virtual关键字,函数定义处可以加也可以不加。
2.为了方便,可以只将基类中的函数声明为虚函数(派生类中可以不加virtual关键字),这样所有派生类中具有遮蔽关系的同名函数都将自动称为虚函数。
3.当在基类中定义了虚函数时,如果派生类没有定义新的函数来遮蔽此函数,那么将使用基类的虚函数。
4.只有派生类的虚函数覆盖基类的虚函数才能构成多态。(通过基类指针访问派生类函数)。例如基类虚函数的原型为:
virtual void func();//注意是无参
派生类虚函数原型为:
virtual void func(int);//注意有参
那么当基类指针p指向派生类对象时,语句p -> func(100);
将会出错,而语句:
p -> func();
将调用基类的函数。
5.构造函数不是虚函数。对于基类的构造函数,它仅仅是派生类构造函数中被调用,这种机制不同于继承。也就是说,派生类不继承基类的构造函数,将构造函数声明为虚函数没有什么意义。
6.析构函数可以声明为虚函数,而且有时候必须要声明为虚函数,后面会讲。
什么时候声明虚函数
首先看成员函数所在的类是否会作为基类。然后看成员函数在类的继承后有无可能被更改功能,如果希望更改其功能的,一般应该将它声明为虚函数。如果成员函数在类被继承后功能不需要修改,或派生类用不到该函数,则不要把它声明为虚函数。
C++纯虚函数和抽象类
在C++中,可以将虚函数声明为纯虚函数,语法格式为:
virtual 返回值类型 函数名(函数参数) = 0;
纯虚函数没有函数体,只有函数声明,在虚函数声明的结尾加上 = 0,表明此函数为纯虚函数。
包含纯虚函数的类的称为抽象类。
抽象类通常是作为基类,让派生类去实现纯虚函数。派生类必须实现纯虚函数才能被实例化。
EX:
class Line {
public:
Line(float len);
virtual float area() = 0;
virtual float volume() = 0;
protected:
float m_len;
};
Line::Line(float len) : m_len(len) {
}
class Rec : public Line {
public:
Rec(float len,float width);
float area();
protected:
float m_width;
};
Rec::Rec(float len,float width):Line(len),m_width(width){
}
float Rec::area() {
return m_len*m_width;
}
//长方体
class Cuboid : public Rec {
public:
Cuboid(float len,float width,float height);
float area();
float volume();
protected:
float m_height;
};
Cuboid::Cuboid(float len,float width,float height):Rec(len,width),m_height(height){
}
float Cuboid::area() {
return 2*(m_len*m_width+m_len*m_height+m_width*m_height);
}
float Cuboid::volume() {
return m_len*m_width*m_height;
}
void testCC(){
Line *p = new Cuboid(10,20,30);
cout<<"The area of Cuboid is "<<p->area()<<endl;
}
int main() {
testCC();
return 1;
}
在实际开发中,你可以定义一个抽象基类,只完成部分功能,未完成的功能交给派生类去实现。
抽象基类除了约束派生类的功能,还可以实现多态。注意p的类型是Line,但是他却可以访问派生类中的area()和volume(),正是由于在Line类中将这两个函数函定义为纯虚函数;如果不这样做,那么后面的代码都是错误的。
关于纯虚函数的几点说明:
1.一个纯虚函数就可以使类称为抽象基类,但是抽象基类中除了包含纯虚函数外,还可以包含其它的成员函数和成员变量。
2.只有类中的虚函数才能被声明为纯虚函数,普通成员函数和顶层函数均不能声明为纯虚函数。
C++ typeid运算符:获取类型信息
typeid运算符用来获取一个表达式的类型信息。类型信息对于编程语言非常重要,它描述了数据的各种属性:
1.对于基本类型的数据,类型信息所包含的内容比较简单,主要是指数据的类型。
2.对于类类型的数据,即对象,类型信息是指对象所属的类、所包含的成员、所在的继承关系等。
类型信息是创建数据的模板,数据占用多大内存、能进行什么样的操作、该如何操作等,这些都由它的类型信息决定。
typeid的操作对象既可以是表达式,也可以是数据类型,两种使用方法如下:
typeid(dataType)
typeid(expression)
这和sizeof运算符非常类似,只不过sizeof有时候可以省略括号,而typeid必须带上括号。
typeid会把获取到的类型信息保存到一个type_info类型的对象里面,并返回该对象的常引用;当需要具体的类型信息时,可以通过成员函数来提取。(类似Java的Class类?).typeid的使用非常灵活,ex:
C++标准只对type_info类做了很有限的规定,成员函数少,功能弱,而且各个平台的实现不一致。
可以发现,不像 Java、C# 等动态性较强的语言,C++ 能获取到的类型信息非常有限,也没有统一的标准,如同“鸡肋”一般,大部分情况下我们只是使用重载过的“==”运算符来判断两个类型是否相同。
由上所述看似typeid有些鸡肋,但实际上在某个方面还是会大量使用typeid关键字的
判断类型是否相等
ex:
例子中可以用来判断基本类型,可以判断类是否相当,如下:
运算符重载
C++运算符重载基础教程
所谓重载,就是赋予新的含义。函数重载(Function Overloading)可以让一个函数名有多种功能,在不同情况下进行不同的操作。运算符重载(Operator Overloading)也是一个道理,同一个运算符可以有不同的功能。
ex:
#include "chapter6.h"
#include <iostream>
using namespace std;
//运算符重载实现复数的加法运算
class complex {
public:
complex();
complex(double real, double imag);
public:
complex operator+(const complex &A) const;
void display() const;
private:
double m_real;//实部
double m_imag;//虚部
};
complex::complex() : m_real(0.0), m_imag(0.0) {}
complex::complex(double real, double imag) : m_real(real), m_imag(imag) {}
//实现运算符重载
complex complex::operator+(const complex &A) const {
complex B;
B.m_real = this->m_real + A.m_real;
B.m_imag = this->m_imag + A.m_imag;
return B;
}
void complex::display() const {
cout<<m_real<<" + "<< m_imag <<"i"<<endl;
}
void testGG(){
complex c1(4.3,5.8);
complex c2(2.4,3.7);
complex c3;
c3 = c1 + c2;
c3.display();
}
int main(){
testGG();
return 0;
}
运算符重载其实就是定义一个函数,在函数体内实现想要的功能,当用到该运算符时,编译器会自动调用这个函数。也就是说,运算符重载是通过函数实现的,它本质上是函数重载。
运算符重载的格式为:
返回值类型 operator 运算符名称 (形参表列){
//TODO:
}
operator是关键字,专门用于定义重载运算符的函数。我们可以将operator 运算符名称
这一部分看作函数名,对于上面的代码,函数名就是operator +
。
运算符重载函数除了函数名有特定的格式,其他地方和普通函数并没有区别
全局范围内重载运算符
运算符重载函数不仅可以作为类的成员函数,还可以作为全局函数。
修改上面代码,ex:
#include <iostream>
using namespace std;
class complex{
public:
complex();
complex(double real, double imag);
public:
void display() const;
//声明为友元函数
friend complex operator+(const complex &A, const complex &B);
private:
double m_real;
double m_imag;
};
complex operator+(const complex &A, const complex &B);
complex::complex(): m_real(0.0), m_imag(0.0){ }
complex::complex(double real, double imag): m_real(real), m_imag(imag){ }
void complex::display() const{
cout<<m_real<<" + "<<m_imag<<"i"<<endl;
}
//在全局范围内重载+
complex operator+(const complex &A, const complex &B){
complex C;
C.m_real = A.m_real + B.m_real;
C.m_imag = A.m_imag + B.m_imag;
return C;
}
int main(){
complex c1(4.3, 5.8);
complex c2(2.4, 3.7);
complex c3;
c3 = c1 + c2;
c3.display();
return 0;
}
运算符重载函数不是complex类的成员函数(虽然是定义在此类之中),但是却用到了complex类的private成员变量,所以必须在complex类中将该函数声明为友元函数。
运算符重载所实现的功能虽然完全可以用函数替代,但运算符重载使得程序书写更加人性化,易于阅读。运算符被重载后,原有的功能仍然保留,没有丧失或改变。
通过运算符重载,扩大了C++已有运算符的功能,使之能用于对象。
运算符重载时要遵循的规则
1.并不是所有的运算符都可以重载,能够重载的运算符包括:
+ - * / % ^ & | ~ ! = < > += -= *= /= %= ^= &= |= << >> <<= >>= == != <= >= && || ++ -- , ->* -> () [] new new[] delete delete[]
长度运算符sizeof
、条件运算符?:
、成员选择符.
和 域解析运算符::
不能被重载。
2.重载不能改变运算符的优先级和结合性
3.重载不会改变运算符的用法,原有有几个操作符、操作数在左边还是右边,这些都不会被改变。例如~
号右边只有一个操作数,+
号总是出现在两个操作数之间,重载后也必须如此。
4.运算符重载函数不能有默认的参数,否则就改变了运算符操作数的个数,这显然是错误的。
5.运算符重载函数既可以作为类的成员函数,也可以作为全局函数。
①当运算符重载函数作为类的成员函数的时候
二元运算符的参数只有一个,一元运算符不需要参数。之所以少一个参数,是因为这个参数是隐含的。
比如之前定义的complex里重载的加法运算符:
complex operator+(const complex &A) const
当执行“
c3 = c1 + c2
会被转换成:
c3 = c1.operator+(c2);
通过this指针隐含的访问c1的成员变量。
②当运算符重载函数作为全局函数时
二元操作符就需要两个参数,一元操作符需要一个参数,而且其中必须有一个参数是对象,防止修改内置类型的运算符的性质。
比如,下面的重载就是错误的:
int operator + (int a,int b) {
return (a-b);
}
如果允许这么重载,表达式4+3
的结果应该是1还是7呢,所以这种做法是不允许的,必须有一个参数是对象。
C++重载数学运算符(实例演示)
四则运算符(+、-、、/、+=、-=、=、/=)和关系运算符(>、<、<=、>=、==、!=)都是数学运算符,被重载的几率很高。
本节还是以Complex复数类进行重载,且因为复数不能比较大小,所以这里不重载>,<,<=,>=运算符了。
到底以成员函数还是全局函数(友元函数)的形式重载运算符
下面的几章TODO,暂时用不到
模板
模板入门教程
在《C++函数重载》中定义了四个名字相同、参数列表不同的四个函数:
//交换 int 变量的值
void Swap(int *a, int *b){
int temp = *a;
*a = *b;
*b = temp;
}
//交换 float 变量的值
void Swap(float *a, float *b){
float temp = *a;
*a = *b;
*b = temp;
}
//交换 char 变量的值
void Swap(char *a, char *b){
char temp = *a;
*a = *b;
*b = temp;
}
//交换 bool 变量的值
void Swap(bool *a, bool *b){
char temp = *a;
*a = *b;
*b = temp;
}
在C++中,数据的类型也可以通过参数来传递,在函数定义时可以不指名具体的数据类型,当发生函数调用时,编译器会自动推断数据类型。这就是类型的参数化(大致可以理解为java的泛型)
所谓函数模板,实际上是建立一个通用函数,它所用到的数据的类型(包括返回值类型、形参类型、局部变量类型)可以用一个虚拟的类型来代替,等发生函数调用时再根据传入的实参来逆推出真正的类型,这个通用函数就被称为函数模板。
一旦定义了函数模板,就可以将类型参数用于函数定义和函数声明了。说的直白一点,原来用int、float、char等内置类型的地方,都可以用类型参数来代替。
使用模板重定义函数:
//
// Created by 18041 on 2022/4/29.
//
#include "chapter7.h"
#include <iostream>
using namespace std;
template<typename T> void swap(T *a, T *b) {
T temp = *a;
*a = *b;
*b = temp;
}
void testM1() {
int n1 = 100, n2 = 200;
swap(&n1, &n2);
cout << "n1:" << n1 << " n2:" << n2 << endl;
char n3 = 'A', n4 = 'B';
swap(&n3, &n4);
cout << "n3:" << n3 << " n4:" << n4 << endl;
}
int main() {
testM1();
return 0;
}
template
是定义函数模板的关键字,它后面紧跟尖括号<>
.
typename
是另外一个关键字,用来声明具体的类型参数,这里的类型参数就是
T
.从整体上看,template<typename T>
被称为模板头。
类型参数的命名规则跟其他标识符的命名规则一样,但使用T/T1/T2/Type等已经成为一种惯例。
上面我们是用指针来实现变量值的交换,后面我们学过引用,也可以通过定义引用的函数模板来达到交换值的目的,ex:
template<typename Type>
void swap2(Type &a, Type &b) {
Type temp = a;
a = b;
b = temp;
}
void testM2() {
int n1 = 100, n2 = 200;
swap2(n1, n2);
cout << "n1:" << n1 << " n2:" << n2 << endl;
float n3 = 20.5, n4 = 48.2;
swap2(n3, n4);
cout << "n3:" << n3 << " n4:" << n4 << endl;
}
总结一下定义模板函数的语法:
template <typename 类型参数1,typename 类型参数2,...> 返回值类型 函数名(形参列表) {
//在函数体中使用类型参数
}
typename
关键字也可以使用class
关键字代替,他们没有任何区别,很多代码仍在使用class关键字,包括C++标准库、一些开源程序等。
C++类模板
C++除了函数模板,还支持类模板。函数模板中定义的类型参数可以用在函数声明和函数定义中,类模板中定义的类型参数可以用在类声明和类实现中。类模板的目的同样是将数据的类型参数化。
声明类模板的语法为:
template<typename 类型参数1,typename 类型参数2,...> class 类名{
//TODO;
}
类模板和参数模板都是以template开头(当然也可以用class),后跟类型参数;参数类型不能为空;
一旦声明了类模板,就可以将类型参数用于类的成员函数和成员变量了。换句说,原来使用int、float、char等内置类型的地方,都可以使用类型参数来替代。
ex:我们要定义一个类来表示坐标,但坐标的类型不确定,可以是整数、小数和字符串,例如:
x = 10 ,y = 100
x = 12.88 ,y = 128.10
x ="东经180度",y = "北纬210度"
那我们就可以定义模板类来实现:
//模板类
template<typename T1, typename T2>
class Point {
public:
Point(T1 x, T2 y) : m_x(x), m_y(y) {}
public:
T1 getX() const;
void setX(T1 x);
T2 getY() const;
void setY(T2 y);
private:
T1 m_x;
T2 m_y;
};
//对成员函数定义
template<typename T1, typename T2>
T1 Point<T1, T2>::getX() const {
return m_x;
}
template<typename T1, typename T2>
T2 Point<T1, T2>::getY() const {
return m_y;
}
除了类的声明之外,我们还需要在类外定义成员函数。在类外定义成员函数时仍然需要带上模板头,格式为:
template<typename 类型参数1,typename 类型参数2,...>
返回值类型 类名<类型参数1,类型参数2,...>::函数名(形参列表){
//TODO
}
大话C++模板编程的来龙去脉
不同的编程语言根据不同的标准可以分为不同的类,根据“在定义变量时是否需要显式地指明数据类型”可以分为【强类型语言】和【弱类型语言】;
1.强类型语言
强类型语言在定义变量时需要显式地指明数据类型,且一旦指定某个数据类型,该变量以后就不能赋予其他类型的数据了,除非强制类型转换或隐式转换。典型的强类型语言有C/C++、Java、C#等。
C/C++中使用变量:
int a = 100;
a = 12.34;//隐式转换,会直接舍去小数部分,得到12
a = (int)"http://c.biancheng.net";//强制转换,会得到字符串地址;
2.弱类型语言
弱类型语言在定义变量时不需要显式地指明数据类型,编译器会根据赋给变量的数据自动推导出类型,并且可以赋给不同类型的数据。典型的弱类型语言有JavaScript、Python、Kotlin、Shell等;
示例这里就不说了。
不管是强类型语言还是弱类型语言,在编译器内部都有一个类型系统来维护变量的各种信息。
对于强类型的语言,变量的类型从始至终都是确定的、不变的,编译器在编译期间就能监测某个变量的操作是否正确,这样最终生成的程序中就不用再维护一套类型信息了,从而减少了内存的使用,加快了程序的运行;但也不绝对,有些特殊情况下还是需要等到运行阶段才能确定变量的类型信息。比如C++中的多台,编译器在编译阶段会在对象内存模型中增加虚函数表、type_info对象等辅助信息,以维护一个完整的继承链,等到程序运行后再执行一段代码才能确定调用哪个函数;
弱类型语言往往是一边执行一边编译,这样便可以根据上下文推到出很多有用的信息,让编译更加高效。这种一边执行一边编译的语言我们称为解释型语言,而将传统的先编译后执行的语言称为编译型语言。
强类型语言较为严谨,在编译时就能发现很多错误,适合开发大型的、系统级、工业级的项目;而弱类型语言较为灵活,编码效率高,部署容易,学习成本地,在Web开发中大显身手;
C++支持模板主要就是为了弥补强类型语言"不够灵活"的缺点;
模板所支持的类型是宽泛的,没有限制的,我们可以使用任意类型来替换,这种编程方式称为泛型编程。(所以就是跟java的泛型编程一个意思。)
C++模板最初推出的直接动力
C++ 模板也是被迫推出的,最直接的动力来源于对数据结构的封装。数据结构关注的是数据的存储,以及存储后如何进行增加、删除、修改和查询操作,它是一门基础性的学科,在实际开发中有着非常广泛的应用。C++ 开发者们希望为线性表、链表、图、树等常见的数据结构都定义一个类,并把它们加入到标准库中,这样以后程序员就不用重复造轮子了,直接拿来使用即可。
但是这个时候遇到了一个无法解决的问题,就是数据结构中每份数据的类型无法提前预测。以链表为例,它的每个节点可以用来存储小数、整数、字符串等,也可以用来存储一名学生、教师、司机等,还可以直接存储二进制数据,这些都是可以的,没有任何限制。而 C++ 又是强类型的,数据的种类受到了严格的限制,这种矛盾是无法调和的。
要想解决这个问题,C++ 必须推陈出新,跳出现有规则的限制,开发新的技术,于是模板就诞生了。模板虽然不是 C++ 的首创,但是却在 C++ 中大放异彩,后来也被 Java、C# 等其他强类型语言采用。
TODO 付费章节
其他章节暂且不细看,我们当下的目的是用起来,而不是深钻。