C++是博主的一门限选课,所使用的课本为谭浩强老师的《C++面向对象程序设计》(第二版),这本书内容全面,简单易懂,也强烈推荐给大家(尤其是初学者)。但对于有Java/C#知识的人来说很多地方的讲解略显冗余。这门课的授课老师编程经验丰富,因此可以跳开课本进行更为简洁精确的说明讲解。虽然是只有8周的限选课,博主依然对课堂知识作了记录,这些都是精华所在,即大家所言的老程序员的经验。
(正文字体采用“等线”,最新版win10可以达到最佳显示效果)
1.使用const定义常量
//C语言定义常量
#define PI 3.1415926**重点内容**
//C++定义常量
const float PI = 3.1415926
2.函数模板
//通用函数定义 类似于Java的泛型
template <typename T> 或者 template <class T> //后者也可以实现模版类
//示例:
template <typename T>
T max(T a, T b, T c) {
if(b > a) a = b;
if(c > a) a = c;
}
//如果不使用函数模板 实现泛型调用 你需要使用指针和2倍数量的参数再加上一个switch分支判断语句 编程复杂 代码可读性非常低
3.有默认参数的构造函数
void f2(float a, int c, int b = 0, char d = 'a');
//实参与形参的结合是从左往右的 因此指定的默认参数必须放在参数列表最右端
4.引用
C++的”引用”就是变量的别名,建立引用是为了避免为一个变量再起一个名字,而Java/C#的引用指的是地址,相当于C/C++的指针,二者区别相当大。
int a;
int &b = a; //声明b是一个整型变量的引用,它初始化为a
int *p; //定义指针p
p = &a;
//定义引用必须直接赋值 定义指针可以稍后赋值
//C++中所有的实参到形参的传递都是值传递,没有例外
5.内置函数inline
函数调用花费一定时间,也占用一些资源,特别是频繁使用的函数会降低程序的执行效率。C++提供的inline关键字可以解决这一问题。在函数声明前加inline即表示这是一个内嵌函数其调用时编译器会将函数体代码代替调用语句,同时将实参代替形参,以提高效率。(详见课本24页)
//使用inline没有系统开销
6.头文件和extern
#inclue <c.h>和#include "c.h"
//前者表示在当前目录下引用头文件 后者表示引用全局变量的头文件,当前目录没有则会去其他目录寻找
extern引用已近声明过的变量(不同文件中)
C++所有类必须以分号”;”结束
7.class 和 struct的区别
class和struct具有相同的功能——定义一个类别,如果用struct定义,则默认所有成员都是public,用class定义则默认里面所有成员都是private。
8.在类外定义成员函数
“::”是域作用符,经常用来表示从属关系,尤其是声明某方法是某类的成员
class Student {
public:
void display();
private:
int num;
string name;
char sex;
};
void Student::display() {
cout << "num:" << num << endl;
cout << "name:" << name << endl;
cout << "sex:" << sex << endl;
}
//在内存布局上,C++类的方法不存放在对象的实例中,仅给数据成员分配空间安
8.构造函数 析构函数
Box box(); //不是定义对象 是函数声明
C++中,构造函数不能在任何地方调用。
//构造函数在对象第一次初始化时调用,其是对象诞生时第一次调用的函数
//构造函数调用的时候,对象已经有了,只是未经初始化。
//对象已经有了 = 内存已经分配
构造函数在编译时就会被调用,即使main()函数为空。因此与C语言不同,即使main()函数为空,只要编译的文件里有构造函数且构造函数有输出,即使main()函数为空程序也有输出。
释放内存一般通过析构函数(Java则有专门的垃圾回收机制)。
//析构函数
~Box() { delete p[]; }
//对象在失去声明体征的时候最后一次调用的函数
//析构函数的调用顺序与构造函数调用的顺序刚好相反 即 先构造的后析构
9.初始化列表
//父类的构造函数、常量、引用、自对象等初始化必须要在初始化列表中进行。
//类的普通变量在构造函数还是在初始化列表中进行没有区别。
//初始化列表: l(ll),w(ww),h(hh) 三个均为普通变量
Box(int ll, int ww, int hh) : l(ll),w(ww),h(hh) {}
//初始化列表: Pnt(xx, yy), pt(xx+1, yy-1) Pnt父类构造函数 pt子对象(pt为Circle中定义的Pnt对象)
Circle(int xx, int yy, int rr) : Pnt(xx, yy), pt(xx+1, yy-1) {
r = rr;
}
10.函数指针
void (*p) ();
p = otprint;
//函数指针 接受符合其参数要求的函数的指针 通常用来将函数作为形参来实现调用
void fun(void (*p)()) {p();}
fun(outprint);
11.常成员函数
如果将成员函数声明为常成员函数,则只能用来引用本类的数据成员,而不能修改他们。
void get_time()const
形式:
类型名 函数名(参数表) const
(相关内容见课本 92页 下同)
12.指针常量和常量指针
int* const p = &c;//指针常量 指针本身不能修改,其指向的内容可以修改
const int* pp = &c//常量指针 其指向的内容不能修改 指针本身可以修改
const int* const ppp = &c//指针和其指向的内容都不能修改
13.对象的赋值和复制
对象的赋值:C++中对象可以使用”=”进行赋值,通过重载”=”号运算符实现,实际上这个过程是通过成员赋值(mewberwise copy)实现的,即将一个对象的成员一一赋值给另一对象的对应成员。注意:对象的赋值只是对其中数据成员的赋值,而不是对成员函数赋值。
对象1 = 对象2;//浅拷贝 浅复制
Box *b1 = new Box(1, 2, 3); //C++类Java对象声明 必须使用指针
Box *b2;
b1 = b2; //浅拷贝
delete b1;
delete b2;//同一块地址 删了2次
Box b1 = Box(1, 2, 3);
Box b2;
b2 = b1;//位复制方式给b2赋值 想当于b1的内存完全copy给b1
//当这里给b2赋值时,回到时b2的p指针和b1的p指针指向同一个地方 即b1的p
//当程序运行结束时,会导致同一个内存在各自的析构函数中释放,累计释放了2次。
//深拷贝 深复制
补充:深拷贝和浅拷贝的区别:
https://www.zhihu.com/question/36370072
对象的复制:用一个已有的对象快速地多个完全相同的对象。
//对象赋值:两个对象都已经存在,然后用一个对象给另一个对赋值
//对象复制:已经有一个对象,再创建另一个对象时,用已知的对象初始化创建对象
对象复制的一种形式:
Box box2(box1);//类名 对象2(对象1);
//用对象1复制出对象2 建立对象时调用了一个特殊的构造函数——copy构造函数(copy constructor)
//the copy constructor definition 详见课本101页
Box :: Box(const Box&b) {
height = b.height;
width = b.width;
length = b.length;
}
对象复制的另一种形式:
Box box2 = box1;//类名 对象1 = 对象2;
两者区别:
对象的赋值是对一个已经存在的对象赋值,因此必须先定义被赋值的对象,才能进行对象的赋值。而对象的复制则是从无到有地建立一个新对象,并使它与一个已有的对象的完全相同(包括对象的结构和成员的值)。
c3 = c1.complex_add(c2);//此处存在对象的赋值和复制问题
//对象的复制,存在于实参到形参以及函数的返回中
//实现机制实例
//重载=号实现对象赋值
Person& operator= (Person &s) {
puts("oper");
if(this == &s) return s;
char *t = new char[srelen(s.p) + 1];
strcpy(t, s.p);
delete []p;
p = t;
return this*;
}
//Copy构造函数实现对象复制
Person (const Person& s) {
puts("cp cntr");
p = new char[strlen(s.p) + 1];
strcpy(p, s.p);
}
14.静态数据成员
静态数据成员是一种特殊的数据成员,以关键字static开头。例如:
class Box {
public:
int volume();
private:
static int height; //height定义为静态数据成员
int width;
int length;
}
//静态成员是属于类的,该类所有的所有实例都共享的的变量。
//静态变量又称类变量 普通变量也称实例变量
//普通变量 是属于对象的
//静态变量不能在构造函数中初始化 只能在全局初始化(类外体)
//int Box:: height = 111; //初始化后才算定义完毕
15.静态成员函数
static int volume();
//静态成员函数主要用来访问静态数据成员,而不访问非静态成员。
16.友元
//友元函数 友元类
//使用friend关键字 把函数或者类赋以较高权限,可以访问类的私有变量
//友元破坏封装 从面向对象角度来说
友元函数:
//example 1 全局函数
#include <iostream>
using namespace std;
class Time{
public:
Time(int , int, int);
friend void display(Time &);//声明display为友元函数
private:
int hour;
int minute;
int sec;
}
Time :: Time(int h, int m, int s) {
hour = h;
minite = m;
sec = s;
}
void display(Time &t) {
cout << t.hour << ":" << "t.minute" << ":" << t.sec << endl;
}
int main() {
Time t1(10, 13, 56);
display(t1);
return 0;
}
//result 10:13:56
//友元的目的在于可以在需要的对方提高性能
//可以是全局函数,也可以是类中的函数
//example 2 友元成员函数
#include <iostream>
using namespace std;
class Date;//对Date类提前声明
class Time{
public:
Time(int , int, int);
void display(Date &);//display为成员函数 此处注意Date必须已知 即进行过声明 即遵循知道有这个东西 才可以拿过来声明的原则
private:
int hour;
int minute;
int sec;
}
class Date{
public:
Date(int , int, int);
friend void Time:: display(Date &);//声明Time中的display为本类的友元函数
private:
int month;
int day;
int year;
}
Time :: Time(int h, int m, int s) {
hour = h;
minite = m;
sec = s;
}
void Time::display(Date &d) {
cout << d.month << "/" << d.day << "/" << d.year << endl;
cout << hour << ":" << "minute" << ":" << sec << endl;
}//注意此处Time Date不仅要求已经声明 而且已经实现 即遵循知道有这个东西 并且了解东西的内部构造 才可以拿过来使用的原则
Date :: Date(int m, int d, int year) {
month = m;
day = d;
year = y;
}
int main() {
Time t1(10, 13, 56);
Date d1(12, 25 2004);
t1.display(d1);
return 0;
}
//result: 12/25/2004\n 10:13:56
友元类:
//在类A的定义体中用一下语句声明B为其友元类
friend B;
//友元B中的所有函数都是A的友元函数
17.运算符重载
为什么要重载,使操作简便。
重载+-*\:意义不大,只是看起来比较舒服。
重载”=”是最有意义的。
//重载+号运算符 实现两个Complex对象相加
Complex Complex::operator+ (Complex &c2) {
Complex c;
c.real = real + c2.real;
c.imag = imag + c2.imag;
return c;
}
c3 = c1 + c2;//编译器将其解释为 c1.operator+(c2)
友元运算符重载:
#include <iostream>
using namespace std;
class Complex{
public:
Complex() { real = 0; imag = 0; }
Complex(double r, double i) { real = r, imag = i; }
friend Complex operator+ (Complex &c1, Complex &c2);//运算符重载函数作为友元函数
void display();
private:
double real;
double imag;
};
Complex operator+ (Complex &c1, Complex &c2) {
return Complex(c1.real + c2.real, c1.imag + c2.imag);
}
void Complex::display() {
cout << "(" << real << "," << imag << "i)" << endl;
}
int main() {
Complex c1(3,4), c2(5, -10), c3;
c3 = c1 + c2; //编译器将其解释为 c1.operator+(c2)
}
//友元重载比一般的重载要好,因为友元函数有访问私有变量的权限
具体详见课本第4章。
18.类型转换函数
Complex(double r) { real = r; imag = 0;}//类型转换构造函数
c = c1 + 2.5; //通过类型转换构造函数实现
operator double() { return real; } ;//类型转换函数
Complex cc;
double d,dd;
cc = c + d; //类重载 友元重载 都可以实现
cc = d + c; //只能友元重载实现
dd = c + d;
dd = d + c; //类型转换函数实现的Complex与double类型变量相加 ,返回值为double
19.继承与派生
声明派生类的一般形式为:
class 派生类名: [继承方式] 基类名 {
派生类新增成员。
}
C++可以选择继承方式,包括public,private,protected。
Java的继承相当于public,只有这一种继承形式,默认且不可选择。
public继承方式: 父类里public成员到子类依然是public,private子类看不到。
protected继承方式: 父类里public成员到子类是protected,private子类看不到。
private继承方式: 父类里public成员到子类是private,private子类看不到。
//在有父类的构造函数、子对象的情况下,如果创建当前的对象
//则构造函数执行的顺序为:父类的构造函数,子对象的构造函数,当前对象的构造函数。
//如果有类A,子类B,B有子对象X,则创建B的对象时,构造函数的执行顺序为A X B
20.多态性与虚函数
抱歉,下面只有概念没有实例,详见课本第6章。
多态:父类调用子类的方法,抽象的程序设计,更为一般的编程。
Java默认是多态的 所有的函数都是多态的(编译时多态和运行时多态,编译时多态指重载,运行时多态指函数根据参数实际类型调用,而不是声明类型)。
C++中,所有函数默认是都不是多态的。
C++使用virtual才能实现多态,C#使用virtual或者abstract才能实现多态
一旦父类方法声明为虚方法(virtual),则子类的同名方法(形参、类型必须完全相同)自动变成虚方法。
//在方法前加virtual关键字即成为虚方法
virtual void size() = 0 //纯虚函数
//C++中具有纯虚函数的类,就是抽象类
//C++只能这样定义抽象类
//抽象类 单纯做模板 不能new的类
C++与Java相比,C++的形参可以是引用,可以是指针,也可以是实际对象。Java的形参只能是引用。
//引用或者指针可以产生多态效果,但是实际对象绝不可能产生。
Java只能通过堆创建对象,C++既可以通过堆,也可以通过栈。
Shape *c = new Shape(5);//堆创建
Shape s = Circle(5);//栈创建
Shape s1 = s;//该操作在Java中不存在(用s实际给s1赋值)
C++中多态只在指针或者引用的情况下产生效果。
//普通函数根据对象声明类型 虚函数通过对象实际类型
同名方法(编译时多态):
同一个类的同名方法:函数名重载
父子类同名的方法(形参的个数,类型必须相同)
如果是非多态的方法(普通方法):子类隐藏父类的方法
如果是多态的方法(虚方法):子类覆盖父类的方法
//在父类析构函数之前加virtual
//父类的虚析构函数,解决子类对象赋值给父类指针后,当delete父类指针时,无法调用子类析构函数的问题。