全局变量和局部变量
全局变量(只在外面声明即可)和局部(内外之分)
#include <iostream>
using namespace std;
// 全局变量声明
int g;
int main ()
{
// 局部变量声明
int a, b;
// 实际定义
a = 10;
b = 20;
g = a + b;
cout << g;
return 0;
}
常量
在 C++ 中,有两种简单的定义常量的方式:
- 使用 #define 预处理器。
- 使用 const 关键字。
signed等修饰符
在 char、int 和 double 数据类型前放置修饰符(
- signed
- unsigned
- long
- short
)
char只能signed和unsigned
double只能long
int都可
修饰符 signed 和 unsigned 也可以作为 long 或 short 修饰符的前缀。例如:unsigned long int。
C++ 允许使用速记符号来声明无符号短整数或无符号长整数。您可以不写 int,只写单词 unsigned、short 或 long,int 是隐含的。
存储类(也是变量修饰)
- static(局部变量:类似成为了全局。全局变量没啥影响吧)
- extern(可多个文件引用)
- mutable
- thread_local (C++11)
您可以使用 extern 关键字在任何地方声明一个变量。虽然您可以在 C++ 程序中多次声明一个变量,但变量只能在某个文件、函数或代码块中被定义一次。(声明就是:通知一下我要在这边用这个东西了)
extern 存储类用于提供一个全局变量的引用,全局变量对所有的程序文件都是可见的。
// 变量声明
extern int a, b;
extern int c;
extern float f;
int main ()
{
// 变量定义
int a, b;
int c;
float f;
// 实际初始化
a = 10;
b = 20;
c = a + b;
与或
&&与
||或
循环
while(条件)
for(;;)
do while do...while 循环与 while 循环类似,但是 do...while 循环会确保至少执行一次循环。
break 、continue、 goto(不要用)
if
if
if-else
switch
嵌套switch
#include <iostream>
using namespace std;
int main ()
{
// 局部变量声明
char grade = 'D';
switch(grade)
{
case 'A' :
cout << "很棒!" << endl;
break;
case 'B' :
case 'C' :
cout << "做得好" << endl;
break;
case 'D' :
cout << "您通过了" << endl;
break;
case 'F' :
cout << "最好再试一下" << endl;
break;
default :
cout << "无效的成绩" << endl;
}
cout << "您的成绩是 " << grade << endl;
return 0;
}
好: Exp1 ? Exp2 : Exp3; 代替if-else
a == 1 ? "男":“女” 不用写cout了这个主意
函数
声明和定义,函数声明在main函数那个文件里,意思是我要用这个函数了
如果函数定义的时候参数有赋值,那是默认值
Lambda 函数与表达式:[ ]( ){ }
[](int x, int y){ return x < y ; }
[]{ ++global_x; }
在一个更为复杂的例子中,返回类型可以被明确的指定如下:
[](int x, int y) -> int { int z = x + y; return z + x; }
数组
double balance[10];
double balance[5] = {1000.0, 2.0, 3.4, 7.0, 50.0};
double balance[] = {1000.0, 2.0, 3.4, 7.0, 50.0};
balance[4] = 50.0;为某个元素赋值
多维数组a[][] =
数组指针
字符和字符串
string和char,字符是char,一个单引号,字符串是sring,两个银行
注意string 的时候需要#include<string>
指针
基本
指针是一个变量,其值为另一个变量的地址
int *ip; /* 一个整型的指针 */
double *dp; /* 一个 double 型的指针 */
float *fp; /* 一个浮点型的指针 */
char *ch; /* 一个字符型的指针 */
指针就是一个地址,常见的......如int *a = &b,b变量的地址为a,
所有指针的值的实际数据类型,不管是整型、浮点型、字符型,还是其他的数据类型,都是一样的,都是一个代表内存地址的长的十六进制数。不同数据类型的指针之间唯一的不同是,指针所指向的变量或常量的数据类型不同。
int a = 10;其地址为ip
ip本身是地址,和&a相等(ip = &a),*ip实际上是地址里的变量值(*ip = 10),定义的时候也可以直接 int *ip = &a
NULL指针
int *ptr = NULL; 在变量声明的时候,如果没有确切的地址可以赋值,为指针变量赋一个 NULL 值是一个良好的编程习惯。
指针的算术运算
指针本身是整数
指针的比较
同一个数组中的不同元素,则可对 p1 和 p2 进行大小比较。数组中指针递增。
注意不要直var++,而应该是ptr++
#include <iostream>
using namespace std;
const int MAX = 3;
int main ()
{
int var[MAX] = {10, 100, 200};
int *ptr;
// 指针中第一个元素的地址
ptr = var;
int i = 0;
while ( ptr <= &var[MAX - 1] )
{
cout << "Address of var[" << i << "] = ";
cout << ptr << endl;
cout << "Value of var[" << i << "] = ";
cout << *ptr << endl;
// 指向上一个位置
ptr++;
i++;
}
return 0;
}
》》
Address of var[0] = 0xbfce42d0
Value of var[0] = 10
Address of var[1] = 0xbfce42d4
Value of var[1] = 100
Address of var[2] = 0xbfce42d8
Value of var[2] = 200
数组指针
数组名字就是地址,也就是数组的指针,这个时候指针+1相当于遍历数组。
#include <iostream>
using namespace std;
const int MAX = 3;
int main ()
{
int var[MAX] = {10, 100, 200};
int *ptr;
// 指针中的数组地址
ptr = var;
for (int i = 0; i < MAX; i++)
{
cout << "Address of var[" << i << "] = ";
cout << ptr << endl;
cout << "Value of var[" << i << "] = ";
cout << *ptr << endl;
// 移动到下一个位置
ptr++;
}
return 0;
}
》》
Address of var[3] = 0xbfdb70f8
Value of var[3] = 200
Address of var[2] = 0xbfdb70f4
Value of var[2] = 100
Address of var[1] = 0xbfdb70f0
Value of var[1] = 10
传递数组给函数:函数的参数为定义长度的数组,不定义长度的数组,指针
函数return一个数组,函数需要定义为指针
int *getRandom( )
{
static int r[10];
// 设置种子
srand( (unsigned)time( NULL ) );
for (int i = 0; i < 10; ++i)
{
r[i] = rand();
cout << r[i] << endl;
}
return r;
}
指针数组:
注意字符串本身为一个数组,也就是地址,因此可以出现在里面。
注意里面是指针,所以可以是字符串和数组
注意这个数组[i] = 那个指针的指向值
#include <iostream>
using namespace std;
const int MAX = 3;
int main ()
{
int var[MAX] = {10, 100, 200};
int *ptr[MAX];
for (int i = 0; i < MAX; i++)
{
ptr[i] = &var[i]; // 赋值为整数的地址
}
for (int i = 0; i < MAX; i++)
{
cout << "Value of var[" << i << "] = ";
cout << *ptr[i] << endl;
}
return 0;
}
#include <iostream>
using namespace std;
const int MAX = 4;
int main ()
{
const char *names[MAX] = {
"Zara Ali",
"Hina Ali",
"Nuha Ali",
"Sara Ali",
};
for (int i = 0; i < MAX; i++)
{
cout << "Value of names[" << i << "] = ";
cout << names[i] << endl;
}
return 0;
》》
Value of names[0] = Zara Ali
Value of names[1] = Hina Ali
Value of names[2] = Nuha Ali
Value of names[3] = Sara Ali
指针的指针:
int **var;
指针/数组作为函数的参数:
double getAverage(int *arr, int size)
从函数返回指针:
int *getRandom( )
返回数组
引用&
基本
引用变量是一个别名,也就是说,它是某个已存在变量的另一个名字。
目的:引用之后都是4个字节,快。
一旦把引用初始化为某个变量,就可以使用该引用名称或变量名称来指向变量。
- 不存在空引用。引用必须连接到一块合法的内存。
- 一旦引用被初始化为一个对象,就不能被指向到另一个对象。指针可以在任何时候指向到另一个对象。
- 引用必须在创建时被初始化(定义)。指针可以在任何时间被初始化(定义)。
- 引用的本质就是一个指针常量(什么是指针常量:指向不可以改变但是指向内存的值可以变)
int& r = i;
double& s = d;
引用作为参数
void swap(int& x, int& y)
{
int temp;
temp = x; /* 保存地址 x 的值 */
x = y; /* 把 y 赋值给 x */
y = temp; /* 把 x 赋值给 y */
return;
}
引用作为返回值
通过使用引用来替代指针,会使 C++ 程序更容易阅读和维护
double& setValues(inti){
特点是可以作为左值
#include <iostream>
using namespace std;
double vals[] = {10.1, 12.6, 33.1, 24.1, 50.0};
double& setValues(int i) {
double& ref = vals[i];
return ref; // 返回第 i 个元素的引用,ref 是一个引用变量,ref 引用 vals[i]
}
//使用的时候可以被赋值
setValues(1) = 20.23; // 改变第 2 个元素
setValues(3) = 70.8;
当返回一个引用时,要注意被引用的对象不能超出作用域。所以返回一个对局部变量的引用是不合法的,但是,可以返回一个对静态变量的引用。
int& func() {
int q;
//! return q; // 在编译时发生错误
static int x;
return x; // 安全,x 在函数作用域外依然是有效的
}
常量引用
加入const变为只读,不可以修改(常量),注意往往修饰形参防止在函数体内被修改。
结构体struct
struct Books
{
char title[50];
char author[50];
char subject[100];
int book_id;
} ;
类似类,
对象对应结构变量(但是首字母注意大写),
也可以用点.
注意:结构体可以看做特殊的类,而且结构体默认是public而不是private
#include <iostream>
#include <cstring>
using namespace std;
// 声明一个结构体类型 Books
struct Books
{
char title[50];
char author[50];
char subject[100];
int book_id;
};
int main( )
{
Books Book1; // 定义结构体类型 Books 的变量 Book1
Books Book2; // 定义结构体类型 Books 的变量 Book2
// Book1 详述
strcpy( Book1.title, "C++ 教程");
strcpy( Book1.author, "Runoob");
strcpy( Book1.subject, "编程语言");
Book1.book_id = 12345;
// Book2 详述
strcpy( Book2.title, "CSS 教程");
strcpy( Book2.author, "Runoob");
strcpy( Book2.subject, "前端技术");
Book2.book_id = 12346;
// 输出 Book1 信息
cout << "第一本书标题 : " << Book1.title <<endl;
cout << "第一本书作者 : " << Book1.author <<endl;
cout << "第一本书类目 : " << Book1.subject <<endl;
cout << "第一本书 ID : " << Book1.book_id <<endl;
// 输出 Book2 信息
cout << "第二本书标题 : " << Book2.title <<endl;
cout << "第二本书作者 : " << Book2.author <<endl;
cout << "第二本书类目 : " << Book2.subject <<endl;
cout << "第二本书 ID : " << Book2.book_id <<endl;
return 0;
}
结构作为函数参数:
注意参数是结构变量
void printBook( struct Books book )
{
cout << "书标题 : " << book.title <<endl;
cout << "书作者 : " << book.author <<endl;
cout << "书类目 : " << book.subject <<endl;
cout << "书 ID : " << book.book_id <<endl;
}
指向结构(变量)的指针
类中既有类的指针(和这个相似),也有对象的指针this
定义的时候是定义结构的指针,但是实际用的时候传入的是结构变量的地址
struct Books *struct_pointer;
现在,您可以在上述定义的指针变量中存储结构变量的地址。
struct_pointer = &Book1;这里是说一定是结构变量的地址而不是结构的地址
为了使用指向该结构的指针访问结构的成员,您必须使用 -> 运算符
struct_pointer->title;
面向对象
基本
class Box
{
public:
double length; // 盒子的长度
double breadth; // 盒子的宽度
double height; // 盒子的高度
};
Box Box1; // 声明 Box1,类型为 Box
Box Box2; // 声明 Box2,类型为 Box
定义类的函数(成员)
里面/外面(用::)
范围解析运算符 ::
需要注意的是,私有的成员和受保护的成员不能使用直接成员访问运算符 (.) 来直接访问,注意点前面应该是对象。::前面是类
public、private、protected 称为访问修饰符
一般是public
private私有(默认不写public时)成员变量或函数在类的外部是不可访问的,甚至是不可查看的。只有类和友元函数可以访问私有成员。
实际操作中,我们一般会在私有区域定义数据,在公有区域定义相关的函数,以便在类的外部也可以调用这些函数.
protected(受保护,子类可以访问)成员变量或函数与私有成员十分相似,但有一点不同,protected(受保护)成员在派生类(即子类)中是可访问的。(类和友元函数和子类)
- 1.public 继承(一一对应):基类 public 成员,protected 成员,private 成员的访问属性在派生类中分别变成:public, protected, private
- 2.protected 继承(public变为protected):基类 public 成员,protected 成员,private 成员的访问属性在派生类中分别变成:protected, protected, private
- 3.private 继承(都变成private):基类 public 成员,protected 成员,private 成员的访问属性在派生类中分别变成:private, private, private
类的构造函数和析构函数
第一个是在对象运行前运行,第二个是最后运行
同类名,析构函数需要~
构造函数常常放一些类的参数
#include <iostream>
using namespace std;
class Line
{
public:
void setLength( double len );
double getLength( void );
Line(double len); // 这是构造函数声明
~Line(); // 这是析构函数声明
private:
double length;
};
// 成员函数定义,包括构造函数
Line::Line(void)
{
cout << "Object is being created" << endl;
}
Line::~Line(void)
{
cout << "Object is being deleted" << endl;
}
void Line::setLength( double len )
{
length = len;
}
double Line::getLength( void )
{
return length;
}
// 程序的主函数
int main( )
{
Line line(10); //这个时候对象赋值,其实就是给了构造函数
// 设置长度
line.setLength(6.0);
cout << "Length of line : " << line.getLength() <<endl; #!!!这里注意private的成员参
数可以通过函数成员改变
return 0;
}
拷贝构造函数
拷贝构造函数是一种特殊的构造函数,它在创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象。拷贝构造函数通常用于:
- 通过使用另一个同类型的对象来初始化新创建的对象。
- 复制对象把它作为参数传递给函数。(对象作为 以值而不是地址为参数的函数 的参数)(注意是复制一份,所以一定是先新建一个在复制一个,调用一个构造在调用拷贝构造)
void doWork(Person p)
{
}
void test()
{
dowork(p);
}
- 复制对象,并从函数返回这个对象。 (一个返回类对象的函数)(注意是复制一份,所以一定是先新建一个在复制一个,调用一个构造在调用拷贝构造)
Person doWork()
{
Person p1;
return p1
}
理解:就是先复制p的所有的东西(成员和参数),然后再调用拷贝构造函数
Line( const Line &line) line是对象
如果在类中没有定义拷贝构造函数,编译器会自行定义一个。如果类带有指针变量,并有动态内存分配,则它必须有一个拷贝构造函数。
#include <iostream>
using namespace std;
class Line
{
public:
int getLength( void );
Line( int len ); // 简单的构造函数
Line( const Line &obj); // 拷贝构造函数
~Line(); // 析构函数
private:
int *ptr;
};
// 成员函数定义,包括构造函数
Line::Line(int len)
{
cout << "调用构造函数" << endl;
// 为指针分配内存
ptr = new int;
*ptr = len;
}
Line::Line(const Line &obj)
{
cout << "调用拷贝构造函数并为指针 ptr 分配内存" << endl;
ptr = new int;
*ptr = *obj.ptr; // 拷贝值
}
Line::~Line(void)
{
cout << "释放内存" << endl;
delete ptr;
}
int Line::getLength( void )
{
return *ptr;
}
void display(Line obj)
{
cout << "line 大小 : " << obj.getLength() <<endl;
}
// 程序的主函数
int main( )
{
Line line(10);
display(line);
return 0;
}
友元函数和友元类
有权访问类的所有私有(private)成员和保护(protected)成员。关键字 friend
友元函数的原型有在类的定义中出现过,但是友元函数并不是成员函数(而是全局函数),在类外面定义 (注意这个时候在类外面不用加作用域了)(注意模板的话可以在类内定义)
友元函数特点: friend void printWidth( Box box ); ()里面需要类的实例
友元函数也是可以直接调用
友元类:下面的成员都是友元函数
class Box
{
double width;
public:
double length;
friend void printWidth( Box box );
void setWidth( double wid );
};
#include <iostream>
using namespace std;
class Box
{
double width;
public:
friend void printWidth( Box box );
void setWidth( double wid );
};
// 成员函数定义
void Box::setWidth( double wid )
{
width = wid;
}
// 请注意:printWidth() 不是任何类的成员函数
void printWidth( Box box )
{
/* 因为 printWidth() 是 Box 的友元,它可以直接访问该类的任何成员 */
cout << "Width of box : " << box.width <<endl;
}
// 程序的主函数
int main( )
{
Box box;
// 使用成员函数设置宽度
box.setWidth(10.0);
// 使用友元函数输出宽度
printWidth( box );
return 0;
}
friend class ClassTwo;
#include <iostream>
using namespace std;
class Box
{
double width;
public:
friend void printWidth(Box box);
friend class BigBox;
void setWidth(double wid);
};
class BigBox
{
public :
void Print(int width, Box box)
{
// BigBox是Box的友元类,它可以直接访问Box类的任何成员
box.setWidth(width);
cout << "Width of box : " << box.width << endl;
}
};
// 成员函数定义
void Box::setWidth(double wid)
{
width = wid;
}
// 请注意:printWidth() 不是任何类的成员函数
void printWidth(Box box)
{
/* 因为 printWidth() 是 Box 的友元,它可以直接访问该类的任何成员 */
cout << "Width of box : " << box.width << endl;
}
// 程序的主函数
int main()
{
Box box;
BigBox big;
// 使用成员函数设置宽度
box.setWidth(10.0);
// 使用友元函数输出宽度
printWidth(box);
// 使用友元类中的方法设置宽度
big.Print(20, box);
getchar();
return 0;
}
内联函数
inline
类定义中的定义的函数都是内联函数,即使没有使用 inline 说明符。
常常对于小的函数,目的是空间代价换时间的节省。(提前放到内存)
注意:
- 1.在内联函数内不允许使用循环语句和开关语句;
- 2.内联函数的定义必须出现在内联函数第一次调用之前;
- 3.类结构中所在的类说明内部定义的函数是内联函数。
this指针
所有类成员都有这个(隐含)
通过 this 指针来访问自己的地址
友元函数没有 this 指针,因为友元不是类的成员。只有成员函数才有 this 指针。
类定义的时候出现
this应该指的是这个类的地址,而不是成员的
#include <iostream>
using namespace std;
class Box
{
public:
// 构造函数定义
Box(double l=2.0, double b=2.0, double h=2.0)
{
cout <<"Constructor called." << endl;
length = l;
breadth = b;
height = h;
}
double Volume()
{
return length * breadth * height;
}
int compare(Box box)
{
return this->Volume() > box.Volume();
}
private:
double length; // Length of a box
double breadth; // Breadth of a box
double height; // Height of a box
};
int main(void)
{
Box Box1(3.3, 1.2, 1.5); // Declare box1
Box Box2(8.5, 6.0, 2.0); // Declare box2
if(Box1.compare(Box2))
{
cout << "Box2 is smaller than Box1" <<endl;
}
else
{
cout << "Box2 is equal to or larger than Box1" <<endl;
}
return 0;
}
指向类的指针
和结构那边类似
#include <iostream>
using namespace std;
class Box
{
public:
// 构造函数定义
Box(double l=2.0, double b=2.0, double h=2.0)
{
cout <<"Constructor called." << endl;
length = l;
breadth = b;
height = h;
}
double Volume()
{
return length * breadth * height;
}
private:
double length; // Length of a box
double breadth; // Breadth of a box
double height; // Height of a box
};
int main(void)
{
Box Box1(3.3, 1.2, 1.5); // Declare box1
Box Box2(8.5, 6.0, 2.0); // Declare box2
Box *ptrBox; // Declare pointer to a class.
// 保存第一个对象的地址
ptrBox = &Box1;
// 现在尝试使用成员访问运算符来访问成员
cout << "Volume of Box1: " << ptrBox->Volume() << endl;
// 保存第二个对象的地址
ptrBox = &Box2;
// 现在尝试使用成员访问运算符来访问成员
cout << "Volume of Box2: " << ptrBox->Volume() << endl;
return 0;
}
类的静态成员
static 关键字
对应参数,可以理解为,所有创建的对象中一个全局变量。
当我们声明类的成员为静态时,这意味着无论创建多少个类的对象,静态成员都只有一个副本,不会初始化。
静态成员在类的所有对象中是1、共享的。
2、外部初始化:我们不能把静态成员的初始化放置在类的定义中,但是可以在类的外部通过使用范围解析运算符 :: 来重新声明静态变量从而对它进行初始化
3、可以直接用类访问,而不用对对象
#include <iostream>
using namespace std;
class Box
{
public:
static int objectCount;
// 构造函数定义
Box(double l=2.0, double b=2.0, double h=2.0)
{
cout <<"Constructor called." << endl;
length = l;
breadth = b;
height = h;
// 每次创建对象时增加 1
objectCount++;
}
double Volume()
{
return length * breadth * height;
}
private:
double length; // 长度
double breadth; // 宽度
double height; // 高度
};
// 初始化类 Box 的静态成员
int Box::objectCount = 0;
int main(void)
{
Box Box1(3.3, 1.2, 1.5); // 声明 box1
Box Box2(8.5, 6.0, 2.0); // 声明 box2
// 输出对象的总数
cout << "Total objects: " << Box::objectCount << endl;
return 0;
}
静态函数只要使用类名加范围解析运算符 :: 就可以访问。
静态成员函数只能访问静态成员数据、其他静态成员函数和类外部的其他函数。
他们不能访问类的 this 指针
使用静态成员函数来判断类的某些对象是否已被创建??
静态成员函数与普通成员函数的区别:
- 静态成员函数没有 this 指针,只能访问静态成员(包括静态成员变量和静态成员函数),可以直接用类访问。
- 普通成员函数有 this 指针,可以访问类中的任意成员
#include <iostream>
using namespace std;
class Box
{
public:
static int objectCount;
// 构造函数定义
Box(double l=2.0, double b=2.0, double h=2.0)
{
cout <<"Constructor called." << endl;
length = l;
breadth = b;
height = h;
// 每次创建对象时增加 1
objectCount++;
}
double Volume()
{
return length * breadth * height;
}
static int getCount()
{
return objectCount;
}
private:
double length; // 长度
double breadth; // 宽度
double height; // 高度
};
// 初始化类 Box 的静态成员
int Box::objectCount = 0;
int main(void)
{
// 在创建对象之前输出对象的总数
cout << "Inital Stage Count: " << Box::getCount() << endl;
Box Box1(3.3, 1.2, 1.5); // 声明 box1
Box Box2(8.5, 6.0, 2.0); // 声明 box2
// 在创建对象之后输出对象的总数
cout << "Final Stage Count: " << Box::getCount() << endl;
return 0;
}
一个派生类继承了所有的基类方法,但下列情况除外:
- 基类的构造函数、析构函数和拷贝构造函数。
- 基类的重载运算符。
- 基类的友元函数。
继承的限定符:
多继承
重载运算符和重载函数
重载函数:
一个函数实现不同的功能
功能类似,所以取同一个名字
参数的个数、类型或者顺序不同才可以
构造函数常常多个名字相同,这里用到了重载函数
重载运算符:更好看,例如+可以解决多个问题的加法而不用建立太多函数
返回类型和参数列表
下面的Box意思是返回Box对象
Box operator+(const Box& c)
Box operator+(const Box& a, const Box& b);
include <iostream>
using namespace std;
class Box
{
public:
double getVolume(void)
{
return length * breadth * height;
}
void setLength( double len )
{
length = len;
}
void setBreadth( double bre )
{
breadth = bre;
}
void setHeight( double hei )
{
height = hei;
}
// 重载 + 运算符,用于把两个 Box 对象相加
Box operator+(const Box& b)
{
Box box;
box.length = this->length + b.length;
box.breadth = this->breadth + b.breadth;
box.height = this->height + b.height;
return box;
}
private:
double length; // 长度
double breadth; // 宽度
double height; // 高度
};
// 程序的主函数
int main( )
{
Box Box1; // 声明 Box1,类型为 Box
Box Box2; // 声明 Box2,类型为 Box
Box Box3; // 声明 Box3,类型为 Box
double volume = 0.0; // 把体积存储在该变量中
// Box1 详述
Box1.setLength(6.0);
Box1.setBreadth(7.0);
Box1.setHeight(5.0);
// Box2 详述
Box2.setLength(12.0);
Box2.setBreadth(13.0);
Box2.setHeight(10.0);
// Box1 的体积
volume = Box1.getVolume();
cout << "Volume of Box1 : " << volume <<endl;
// Box2 的体积
volume = Box2.getVolume();
cout << "Volume of Box2 : " << volume <<endl;
// 把两个对象相加,得到 Box3
Box3 = Box1 + Box2;
// Box3 的体积
volume = Box3.getVolume();
cout << "Volume of Box3 : " << volume <<endl;
return 0;
}
Box operator+(const Box& b)
{
Box box;
box.length = this->length + b.length;
box.breadth = this->breadth + b.breadth;
box.height = this->height + b.height;
return box;
}
// 重载-
Distance operator- ()
{
feet = -feet;
inches = -inches;
return Distance(feet, inches);
}
// 重载小于运算符( < )
bool operator <(const Distance& d)
{
if(feet < d.feet)
{
return true;
}
if(feet == d.feet && inches < d.inches)
{
return true;
}
return false;
}
// 重载<<,>>
friend ostream &operator<<( ostream &output,
const Distance &D )
{
output << "F : " << D.feet << " I : " << D.inches;
return output;
}
friend istream &operator>>( istream &input, Distance &D )
{
input >> D.feet >> D.inches;
return input;
}
};
int main()
{
Distance D1(11, 10), D2(5, 11), D3;
cout << "Enter the value of object : " << endl;
cin >> D3;
cout << "First Distance : " << D1 << endl;
cout << "Second Distance :" << D2 << endl;
cout << "Third Distance :" << D3 << endl;
return 0;
}
多态与虚函数
class Shape {
protected:
int width, height;
public:
Shape( int a=0, int b=0)
{
width = a;
height = b;
}
virtual int area()
{
cout << "Parent class area :" <<endl;
return 0;
}
};
class Shape {
protected:
int width, height;
public:
Shape( int a=0, int b=0)
{
width = a;
height = b;
}
// pure virtual function
virtual int area() = 0;
};
数据封装和数据抽象
数据封装是一种把数据和操作数据的函数捆绑在一起的机制,数据抽象是一种仅向用户暴露接口而把具体的实现细节隐藏起来的机制。
数据隐藏(OOP),安全。
接口
涉及private/public/protected
通常情况下,我们都会设置类成员状态为私有(private),除非我们真的需要将其暴露,这样才能保证良好的封装性。
这一节都是说了类的封装特性
C++接口(抽象类)
抽象类ABC(必须有纯虚函数) -> 接口 (作为基类,不能对象,只能继承(具体类))
#include <iostream>
using namespace std;
// 基类
class Shape
{
public:
// 提供接口框架的纯虚函数
virtual int getArea() = 0;
void setWidth(int w)
{
width = w;
}
void setHeight(int h)
{
height = h;
}
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;
}
};
int main(void)
{
Rectangle Rect;
Triangle Tri;
Rect.setWidth(5);
Rect.setHeight(7);
// 输出对象的面积
cout << "Total Rectangle area: " << Rect.getArea() << endl;
Tri.setWidth(5);
Tri.setHeight(7);
// 输出对象的面积
cout << "Total Triangle area: " << Tri.getArea() << endl;
return 0;
}
这个架构也使得新的应用程序可以很容易地被添加到系统中,即使是在系统被定义之后依然可以如此。
C++文件
#include <fstream>
#include <iostream>
using namespace std;
int main ()
{
char data[100];
// 以写模式打开文件
ofstream outfile;
outfile.open("afile.dat");
cout << "Writing to the file" << endl;
cout << "Enter your name: ";
cin.getline(data, 100); // getline()函数从外部读取一行,
ignore() 函数会忽略掉之前读语句留下的多余字符。
// 向文件写入用户输入的数据
outfile << data << endl;
cout << "Enter your age: ";
cin >> data;
cin.ignore();
// 再次向文件写入用户输入的数据
outfile << data << endl;
// 关闭打开的文件
outfile.close();
// 以读模式打开文件
ifstream infile;
infile.open("afile.dat");
cout << "Reading from the file" << endl;
infile >> data;
// 在屏幕上写入数据
cout << data << endl;
// 再次从文件读取数据,并显示它
infile >> data;
cout << data << endl;
// 关闭打开的文件
infile.close();
return 0;
}
ofstream outfile;
outfile.open("file.dat", ios::out | ios::trunc );
ifstream afile;
afile.open("file.dat", ios::out | ios::in );
ios::app | 追加模式。所有写入都追加到文件末尾。 |
ios::ate | 文件打开后定位到文件末尾。 |
ios::in | 打开文件用于读取。 |
ios::out | 打开文件用于写入。 |
ios::trunc | 如果该文件已经存在,其内容将在打开文件之前被截断,即把文件长度设为 0。 |
istream 和 ostream 类,下面的fileObject是对象
成员函数:seekg和seekp( istream 的 seekg("seek get")和关于 ostream 的 seekp("seek put"))
ios::beg(默认的,从流的开头开始定位),
ios::cur(从流的当前位置开始定位),
ios::end(从流的末尾开始定位)。
// 定位到 fileObject 的第 n 个字节(假设是 ios::beg)
fileObject.seekg( n );
// 把文件的读指针从 fileObject 当前位置向后移 n 个字节
fileObject.seekg( n, ios::cur );
// 把文件的读指针从 fileObject 末尾往回移 n 个字节
fileObject.seekg( n, ios::end );
// 定位到 fileObject 的末尾
fileObject.seekg( 0, ios::end );
异常
try
{
// 保护代码
}catch( ExceptionName e )
{
// 处理 ExceptionName 异常的代码
}
try、catch、throw。
throw:抛出异常
double division(int a, int b)
{
if( b == 0 )
{
throw "Division by zero condition!";
}
return (a/b);
}
try catch
try
{
// 保护代码 #需要检查的代码
}catch( ExceptionName e ) #可能的问题
{
// 处理 ExceptionName 异常的代码 #遇到这个问题怎么办
}
注意:
可以有多个catch
如果catch里面是...,意思是处理任何异常,只要有异常就进入catch里
#include <iostream>
using namespace std;
double division(int a, int b)
{
if( b == 0 )
{
throw "Division by zero condition!";
}
return (a/b); ##throw之后相当于break了,退出函数
}
int main ()
{
int x = 50;
int y = 0;
double z = 0;
try {
z = division(x, y);
cout << z << endl;
}catch (const char* msg) {
cerr << msg << endl;
}
return 0;
}
C++标准异常
异常 | 描述 |
std::exception | 该异常是所有标准 C++ 异常的父类。 |
std::bad_alloc | 该异常可以通过 new 抛出。 |
std::bad_cast | 该异常可以通过 dynamic_cast 抛出。 |
std::bad_exception | 这在处理 C++ 程序中无法预期的异常时非常有用。 |
std::bad_typeid | 该异常可以通过 typeid 抛出。 |
std::logic_error | 理论上可以通过读取代码来检测到的异常。 |
std::domain_error | 当使用了一个无效的数学域时,会抛出该异常。 |
std::invalid_argument | 当使用了无效的参数时,会抛出该异常。 |
std::length_error | 当创建了太长的 std::string 时,会抛出该异常。 |
std::out_of_range | 该异常可以通过方法抛出,例如 std::vector 和 std::bitset<>::operator[]()。 |
std::runtime_error | 理论上不可以通过读取代码来检测到的异常。 |
std::overflow_error | 当发生数学上溢时,会抛出该异常。 |
std::range_error | 当尝试存储超出范围的值时,会抛出该异常。 |
std::underflow_error | 当发生数学下溢时,会抛出该异常。 |
定义新的异常
继承和重载 exception
#include <iostream>
#include <exception>
using namespace std;
struct MyException : public exception
{
const char * what () const throw ()
{
return "C++ Exception";
}
};
int main()
{
try
{
throw MyException();
}
catch(MyException& e)
{
std::cout << "MyException caught" << std::endl;
std::cout << e.what() << std::endl;
}
catch(std::exception& e)
{
//其他的错误
}
}
动态内存和内存四区
内存分区模型
C++程序在执行时,将内存大方向划分为4个区域
静态区:
- 代码区:存放函数体的二进制代码,由操作系统进行管理的
- 全局区:存放全局变量和静态变量以及常量
动态区:
- 栈区:由编译器自动分配释放, 存放函数的参数值,局部变量(包括局部常量)等
- 堆区:由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收
内存四区意义:
不同区域存放的数据,赋予不同的生命周期, 给我们更大的灵活编程
1.1 程序运行前
在程序编译后,生成了exe可执行程序,未执行该程序前分为两个区域
代码区:
存放 CPU 执行的机器指令
代码区是共享的,共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可
代码区是只读的,使其只读的原因是防止程序意外地修改了它的指令
全局区:
全局变量和静态变量存放在此.
全局区还包含了常量区, 字符串常量和其他常量也存放在此.
注意const修饰的全局常量在全局区,const修饰的局部变量不在全局区
该区域的数据在程序结束后由操作系统释放.
示例:
//全局变量
int g_a = 10;
int g_b = 10;
//全局常量
const int c_g_a = 10;
const int c_g_b = 10;
int main() {
//局部变量
int a = 10;
int b = 10;
//打印地址
cout << "局部变量a地址为: " << (int)&a << endl;
cout << "局部变量b地址为: " << (int)&b << endl;
cout << "全局变量g_a地址为: " << (int)&g_a << endl;
cout << "全局变量g_b地址为: " << (int)&g_b << endl;
//静态变量
static int s_a = 10;
static int s_b = 10;
cout << "静态变量s_a地址为: " << (int)&s_a << endl;
cout << "静态变量s_b地址为: " << (int)&s_b << endl;
cout << "字符串常量地址为: " << (int)&"hello world" << endl;
cout << "字符串常量地址为: " << (int)&"hello world1" << endl;
cout << "全局常量c_g_a地址为: " << (int)&c_g_a << endl;
cout << "全局常量c_g_b地址为: " << (int)&c_g_b << endl;
const int c_l_a = 10;
const int c_l_b = 10;
cout << "局部常量c_l_a地址为: " << (int)&c_l_a << endl;
cout << "局部常量c_l_b地址为: " << (int)&c_l_b << endl;
system("pause");
return 0;
}
打印结果:
总结:
- C++中在程序运行前分为全局区和代码区
- 代码区特点是共享和只读
- 全局区中存放全局变量、静态变量、常量
- 常量区中存放 const修饰的全局常量 和 字符串常量
1.2 程序运行后
栈区:
由编译器自动分配释放, 存放函数的参数值,局部变量等
注意事项:不要返回局部变量的地址return &a;,栈区开辟的数据由编译器自动释放()
示例:
int * func()
{
int a = 10;
return &a;
}
int main() {
int *p = func();
cout << *p << endl; 这个可以,输出10,编译器做了一次保留,但是知道即可,平时用的时候不要
cout << *p << endl; 这个不行了,因为
system("pause");
return 0;
}
堆区:
由程序员分配释放,若程序员不释放,程序结束时由操作系统回收
在C++中主要利用new在堆区开辟内存
示例:
int* func()
{
int* a = new int(10);
return a;
}
int main() {
int *p = func();
cout << *p << endl;
cout << *p << endl;
system("pause");
return 0;
}
总结:
堆区数据由程序员管理开辟和释放
堆区数据利用new关键字进行开辟内存
1.3 new操作符
C++中利用new操作符在堆区开辟数据
堆区开辟的数据,由程序员手动开辟,手动释放,释放利用操作符 delete
语法:new 数据类型
利用new创建的数据,会返回该数据对应的类型的指针
示例1: 基本语法
int* func()
{
int* a = new int(10);
return a;
}
int main() {
int *p = func();
cout << *p << endl;
cout << *p << endl;
//利用delete释放堆区数据
delete p;
//cout << *p << endl; //报错,释放的空间不可访问
system("pause");
return 0;
}
示例2:开辟数组
//堆区开辟数组
int main() {
int* arr = new int[10];
for (int i = 0; i < 10; i++)
{
arr[i] = i + 100;
}
for (int i = 0; i < 10; i++)
{
cout << arr[i] << endl;
}
//释放数组 delete 后加 []
delete[] arr;
system("pause");
return 0;
}
了解动态内存在 C++ 中是如何工作的是成为一名合格的 C++ 程序员必不可少的。C++ 程序中的内存分为两个部分:
- 栈:在函数内部声明的所有变量都将占用栈内存。
- 堆:这是程序中未使用的内存,在程序运行时可用于动态分配内存。
在 C++ 中,您可以使用特殊的运算符为给定类型的变量在运行时分配堆内的内存,这会返回所分配的空间地址。这种运算符即 new 运算符。如果您不再需要动态分配的内存空间,可以使用 delete 运算符,删除之前由 new 运算符分配的内存。
实例:
double* pvalue = NULL;
pvalue= new double;
*pvalue = 29494.99; // 在分配的地址存储值
delete pvalue; // 释放内存
如果自由存储区已被用完,可能无法成功分配内存。所以建议检查 new 运算符是否返回 NULL 指针,并采取以下适当的操作:
double *pvalue = NULL;
if (!(pvalue = new double))
{
cout << "Error:out of memort." << enddl;
exit(1) #表示异常退出,0表示正常退出程序
}
delect pvalue; #释放内存
数组的动态内存分配(new,堆区)
假设我们要为一个字符数组(一个有 20 个字符的字符串)分配内存
char *pvalue = NULL;
pvalue = new char[20];
delect [] pvalue;
一维数组:
int *array = new int [m];
delect [] array;
二维数组:
int **array
// 假定数组第一维长度为 m, 第二维长度为 n
// 动态分配空间
array = new int *[m];
for(int i = 0;i<m;i++){
array[i] = new int [n];
}
//释放
for (int i = 0;i<m;i++)
{
delect [] array[i];
}
delete [] array;
实例:
#include <iostream>
using namespace std;
int main()
{
int **p;
int i,j; //p[4][8]
//开始分配4行8列的二维数据
p = new int *[4];
for(i=0;i<4;i++){
p[i]=new int [8];
}
for(i=0; i<4; i++){
for(j=0; j<8; j++){
p[i][j] = j*i;
}
}
//打印数据
for(i=0; i<4; i++){
for(j=0; j<8; j++)
{
if(j==0) cout<<endl;
cout<<p[i][j]<<"\t";
}
}
//开始释放申请的堆
for(i=0; i<4; i++){
delete [] p[i];
}
delete [] p;
return 0;
}
三维数组:
int ***array;
// 假定数组第一维为 m, 第二维为 n, 第三维为h
// 动态分配空间
array = new int **[m];
for (int i = 0 ; i<m;i++)
{
array[i] = new int *[n];
for (int j = 0;j<n;j++)
{
array[i][j] = new int [h];
}
}
//释放
for (int i =0;i<m;i++)
{
for (int j=0lj<n;j++)
{
delect [] array[i][j]
}
delect[]array[i];
}
delect [] array;
三维实例:
#include <iostream>
using namespace std;
int main()
{
int i,j,k; // p[2][3][4]
int ***p;
p = new int **[2];
for(i=0; i<2; i++)
{
p[i]=new int *[3];
for(j=0; j<3; j++)
p[i][j]=new int[4];
}
//输出 p[i][j][k] 三维数据
for(i=0; i<2; i++)
{
for(j=0; j<3; j++)
{
for(k=0;k<4;k++)
{
p[i][j][k]=i+j+k;
cout<<p[i][j][k]<<" ";
}
cout<<endl;
}
cout<<endl;
}
// 释放内存
for(i=0; i<2; i++)
{
for(j=0; j<3; j++)
{
delete [] p[i][j];
}
}
for(i=0; i<2; i++)
{
delete [] p[i];
}
delete [] p;
return 0;
}
对象的动态内存分配
#include <iostream>
using namespace std;
class Box
{
public:
Box() {
cout << "调用构造函数!" <<endl;
}
~Box() {
cout << "调用析构函数!" <<endl;
}
};
int main( )
{
Box* myBoxArray = new Box[4];
delete [] myBoxArray; // 删除数组
return 0;
}
如果要为一个包含四个 Box 对象的数组分配内存,构造函数将被调用 4 次,同样地,当删除这些对象时,析构函数也将被调用相同的次数(4次)。
当上面的代码被编译和执行时,它会产生下列结果:
调用构造函数!
调用构造函数!
调用构造函数!
调用构造函数!
调用析构函数!
调用析构函数!
调用析构函数!
调用析构函数!
C++ 命名空间
例如,您可能会写一个名为 xyz() 的函数,在另一个可用的库中也存在一个相同的函数 xyz()。这样,编译器就无法判断您所使用的是哪一个 xyz() 函数。本质上,命名空间就是定义了一个范围。
我们举一个计算机系统中的例子,一个文件夹(目录)中可以包含多个文件夹,每个文件夹中不能有相同的文件名,但不同文件夹中的文件可以重名。
定义命名空间
#include <iostream>
using namespace std;
// 第一个命名空间
namespace first_space{
void func(){
cout << "Inside first_space" << endl;
}
}
// 第二个命名空间
namespace second_space{
void func(){
cout << "Inside second_space" << endl;
}
}
int main ()
{
// 调用第一个命名空间中的函数
first_space::func();
// 调用第二个命名空间中的函数
second_space::func();
return 0;
}
命名空间有点像Python的的库
using 指令
省略::
#include <iostream>
using namespace std;
// 第一个命名空间
namespace first_space{
void func(){
cout << "Inside first_space" << endl;
}
}
// 第二个命名空间
namespace second_space{
void func(){
cout << "Inside second_space" << endl;
}
}
using namespace first_space;
int main ()
{
// 调用第一个命名空间中的函数
func();
return 0;
}
using 指令也可以用来指定命名空间中的特定项目。例如,如果您只打算使用 std 命名空间中的 cout 部分,您可以使用如下的语句:
using std::cout;
文件里可以不写using namespace,因为可以直接在用的时候把命名空间写在代码里就可以
不连续的命名空间
命名空间可以定义在几个不同的部分中,因此命名空间是由几个单独定义的部分组成的。一个命名空间的各个组成部分可以分散在多个文件中。
所以,如果命名空间中的某个组成部分需要请求定义在另一个文件中的名称,则仍然需要声明该名称。下面的命名空间定义可以是定义一个新的命名空间,也可以是为已有的命名空间增加新的元素:
namespace namespace_name { //不同的文件夹中相同的名字√
// 代码声明
}
嵌套的命名空间
namespace namespace_name1 {
// 代码声明
namespace namespace_name2 {
// 代码声明
}
}
您可以通过使用 :: 运算符来访问嵌套的命名空间中的成员:
注意:
全局变量 a 表达为 ::a,用于当有同名的局部变量时来区别两者。
补充关于 using 的错误事例:
#include <iostream>
using namespace std;
namespace A {
int a = 100;
namespace B //嵌套一个命名空间B
{
int a = 20;
}
}
int a = 200;//定义一个全局变量
int main(int argc, char *argv[]) {
cout << "A::a =" << A::a << endl; //A::a =100
cout << "A::B::a =" << A::B::a << endl; //A::B::a =20
cout << "a =" << a << endl; //a =200
cout << "::a =" << ::a << endl; //::a =200
using namespace A;
cout << "a =" << a << endl; // Reference to 'a' is ambiguous // 命名空间冲突,编译期错误
cout << "::a =" << ::a << endl; //::a =200
int a = 30;
cout << "a =" << a << endl; //a =30
cout << "::a =" << ::a << endl; //::a =200
//即:全局变量 a 表达为 ::a,用于当有同名的局部变量时来区别两者。
using namespace A;
cout << "a =" << a << endl; // a =30 // 当有本地同名变量后,优先使用本地,冲突解除
cout << "::a =" << ::a << endl; //::a =200
return 0;
}
模板和STL
模板
好处:避免指定类型
学习不是为了写模板而是用stl的模板
函数模板
template <typenameT>
void mySwap(T &a,T &b)
{
T temp = a;
a = b;
b = temp;
}
1、mySwap(a,b) # 自动类型推导
2、mySwap<int>(a,b) #指定类型推导
注意事项:
T自动推导:需要相同的数据类型
T必须被指定类型,模板不能只能被用
函数模板也可以重载
函数模板和普通函数的区别:
#include<iostream>
#include<string>
using namespace std;
//
template<class T>
void mySwap(T &a, T&b)
{
T temp = a;
a = b;
b = temp;
}
template<class T>
void mySort(T array[], int len)
{
for (int i = 0; i < len; i++)
{
int max = i;
for (int j = i + 1; j < len; j++)
{
if (array[max] < array[j])
{
max = j;
}
}
if (max != i)
{
mySwap(array[max], array[i]);
}
}
}
template<class T>
void printArray(T arr[], int len)
{
for (int i = 0; i < len; i++)
{
cout << arr[i] << endl;
}
}
void test01()
{
char charArr[] = { "fgfdg" };
int len = sizeof(charArr) / sizeof(char); #数组的长度只能用sizeof,字符串的可以.size()
mySort(charArr, len);
printArray(charArr, len);
}
void test02()
{
int intArr[] = { 6,6,4,51,31,31 };
int len = sizeof(intArr) / sizeof(int);
mySort(intArr, len);
printArray(intArr, len);
}
int main()
{
test02();
}
注意点:
普通函数可以隐式类型转换(比如定义的时候是int,但是传的时候是char,这个时候自动变成int,int可以和char相加(int 函数里,那么会把char装换))
函数模板如果自动类型推导,则没有隐式类型转换
函数模板如果指定类型(如<int>),则有隐式类型转换
普通函数和函数模板的调用规则:
二者可以同时存在
1、如果二者都可以实现,优先普通函数(即使没有定义这个函数),注意这个优先指的是二者参数类型个数什么的都一样的情况下
2、可以用空模板参数列表(<>,里面没有东西)来强制调用函数模板,即加上<>即可
3、函数模板也可以发生重载,这个时候不用管<>,也会自动选择函数模板(因为只有这个参数匹配)
4、如果函数模板可以产生更好的匹配,优先调用函数模板,例如参数类型和普通函数不同,这个时候优先模板,即使普通函数可以发生隐式转换。
void myprint(int a,int b)
{
cout << "调用的普通函数" << endl;
}
template<class T>
void myprint(T a,T b)
{
cout << "调用的函数模板" << endl;
}
void myprint(int a,int b)
{
cout << "调用的普通函数" << endl;
}
template<class T>
void myprint(T a,T b)
{
cout << "调用的函数模板" << endl;
}
template<class T>
void myprint(T a, T b,T c)
{
cout << "调用的重载的函数模板" << endl;
}
int main()
{
myprint(1,2,3);
}
#include<iostream>
using namespace std;
void myprint(int a,int b)
{
cout << "调用的普通函数" << endl;
}
template<class T>
void myprint(T a,T b)
{
cout << "调用的函数模板" << endl;
}
template<class T>
void myprint(T a, T b,T c)
{
cout << "调用的重载的函数模板" << endl;
}
int main()
{
myprint('a','b');
}
总结:尽量不用上面这样普通和模板同名
函数模板的局限性
不是万能的
例如两个比较大小,如果传过去的是两个类的对象:
- 用重载解决
- 用重载
template<> bool myCompare(Person &p1,Person &p2)
解释:C++模板函数的自定义参数类型_c++ 模板类bool 参数-CSDN博客
类模板
看类里面参数有几种数据类型
template<class NameType,class AgeType> //参数有几种,注意是几种,里面就有几个
class Person
{
public:
Person(NameType name, AgeType age)
{
this->m_Name = name;
this->m_Age = age;
}
private:
NameType m_Name;
AgeType m_Age;
};
#include<iostream>
#include<string>
using namespace std;
template<class NameType,class AgeType>
class Person
{
public:
Person(NameType name, AgeType age)
{
this->m_Name = name;
this->m_Age = age;
}
void showperson()
{
cout << "name:" << this->m_Age << "\n";
cout << "age:" << this->m_Name << endl;
}
private:
NameType m_Name;
AgeType m_Age;
};
void test01()
{
Person<string, int>p1("孙悟空", 999);
Person p("孙悟空", 999) //错误,无法使用自动类型推导
p1.showperson();
}
int main()
{
test01();
}
类模板和函数模板的区别
类模板无法自动类型推导
在默认参数列表可以有默认参数template<class NameType,class AgeType>
,那么下面就可以
Person p("孙悟空", 999)
普通类和类模板成员函数创建时机
类模板中成员函数和普通类中成员函数创建时机是有区别的:
- 普通类中的成员函数一开始就可以创建
- 类模板中的成员函数在调用时才创建
示例:
class Person1
{
public:
void showPerson1()
{
cout << "Person1 show" << endl;
}
};
class Person2
{
public:
void showPerson2()
{
cout << "Person2 show" << endl;
}
};
template<class T>
class MyClass
{
public:
T obj;
//类模板中的成员函数,并不是一开始就创建的,而是在模板调用时再生成
void fun1() {
obj.showPerson1();
}
void fun2() {
obj.showPerson2(); //这个时候还没有创建,所以如果不写下面的,编译可以通过
}
};
void test01()
{
MyClass<Person1> m; m对象()里是Person1
m.fun1();
//m.fun2();//编译会出错,说明函数调用才会去创建成员函数
}
int main() {
test01();
system("pause");
return 0;
}
总结:类模板中的成员函数并不是一开始就创建的,在调用时才去创建
普通:类创建时
模板:调用时才创建
类模板对象做函数参数
学习目标:
- 类模板实例化出的对象,向函数传参的方式
一共有三种传入方式:
- 指定传入的类型 --- 直接显示对象的数据类型(最多的)
- 参数模板化 --- 将对象中的参数变为模板进行传递
- 整个类模板化 --- 将这个对象类型 模板化进行传递
示例:
#include <string>
//类模板
template<class NameType, class AgeType = int>
class Person
{
public:
Person(NameType name, AgeType age)
{
this->mName = name;
this->mAge = age;
}
void showPerson()
{
cout << "name: " << this->mName << " age: " << this->mAge << endl;
}
public:
NameType mName;
AgeType mAge;
};
//1、指定传入的类型(最多最清晰)
void printPerson1(Person<string, int> &p)
{
p.showPerson();
}
/*
void printPerson1(Person<string, int> p)
{
p.showPerson();
}
或
void printPerson1(Person<string, int> *p)
{
p->showPerson();
}当然这个时候下面需要&
*/
void test01()
{
Person <string, int >p("孙悟空", 100);
printPerson1(p);
}
//2、参数模板化
template <class T1, class T2>
void printPerson2(Person<T1, T2>&p)
{
p.showPerson();
cout << "T1的类型为: " << typeid(T1).name() << endl;
cout << "T2的类型为: " << typeid(T2).name() << endl;
}
void test02()
{
Person <string, int >p("猪八戒", 90);
printPerson2(p);
}
//3、整个类模板化(注意拓展了思维,模板不仅仅可以是数据类型)
template<class T>
void printPerson3(T & p)
{
cout << "T的类型为: " << typeid(T).name() << endl;
p.showPerson();
}
void test03()
{
Person <string, int >p("唐僧", 30);
printPerson3(p);
}
int main() {
test01();
test02();
test03();
system("pause");
return 0;
}
类模板与继承
当类模板碰到继承时,需要注意一下几点:
(这个时候子类不是模板)
- 当子类继承的父类是一个类模板时,子类在声明的时候,要指定出父类中T的类型
- 如果不指定,编译器无法给子类分配内存
(这个时候子类是模板)
- 如果想灵活指定出父类中T的类型,子类也需变为类模板
示例:
template<class T>
class Base
{
T m;
};
//class Son:public Base //错误,c++编译需要给子类分配内存,必须知道父类中T的类型才可以向下继承
class Son :public Base<int> //必须指定一个类型
{
};
void test01()
{
Son c;
}
//类模板继承类模板 ,可以用T2指定父类中的T类型
template<class T1, class T2>
class Son2 :public Base<T2> (多个可选的类型了)
{
public:
Son2()
{
cout << typeid(T1).name() << endl;
cout << typeid(T2).name() << endl;
}
};
void test02()
{
Son2<int, char> child1;
}
int main() {
test01();
test02();
system("pause");
return 0;
}
总结:如果父类是类模板,子类需要指定出父类中T的数据类型
类模板成员函数类外实现
学习目标:能够掌握类模板中的成员函数类外实现
示例:
#include <string>
//类模板中成员函数类外实现
template<class T1, class T2>
class Person {
public:
Person(T1 name, T2 age);//构造函数类内声明
void showPerson();//成员函数类内声明
public:
T1 m_Name;
T2 m_Age;
};
//构造函数 类外实现
template<class T1, class T2>
Person<T1, T2>::Person(T1 name, T2 age) {
this->m_Name = name;
this->m_Age = age;
}
//成员函数 类外实现
template<class T1, class T2>
void Person<T1, T2>::showPerson()
{
cout << "姓名: " << this->m_Name << " 年龄:" << this->m_Age << endl;
} //
void test01()
{
Person<string, int> p("Tom", 20);
p.showPerson();
}
int main() {
test01();
system("pause");
return 0;
}
总结:类模板中成员函数类外实现时,需要加上模板参数列表template<class T1, class T2>
类模板成员函数分文件(hpp文件)编写
学习目标:
- 掌握类模板成员函数分文件编写产生的问题以及解决方式
问题:
- 类模板中成员函数创建时机是在调用阶段(普通函数就是全局的了,类模板中成员函数不是),导致分文件编写时链接不到(只连接.h文件,没有让计算机看到类模板中成员函数的定义)
解决:
- 解决方式1:直接包含.cpp源文件(这个cpp里放的是类模板成员函数的实现)
- 解决方式2:将声明和实现写到同一个文件中,并更改后缀名为.hpp,hpp是约定的名称,并不是强制
示例:
person.hpp中代码:
#pragma once
#include <iostream>
using namespace std;
#include <string>
template<class T1, class T2>
class Person {
public:
Person(T1 name, T2 age);
void showPerson();
public:
T1 m_Name;
T2 m_Age;
};
//构造函数 类外实现
template<class T1, class T2>
Person<T1, T2>::Person(T1 name, T2 age) {
this->m_Name = name;
this->m_Age = age;
}
//成员函数 类外实现
template<class T1, class T2>
void Person<T1, T2>::showPerson() {
cout << "姓名: " << this->m_Name << " 年龄:" << this->m_Age << endl;
}
类模板分文件编写.cpp中代码
#include<iostream>
using namespace std;
//#include "person.h"
#include "person.cpp" //解决方式1,包含cpp源文件
//解决方式2,将声明和实现写到一起,文件后缀名改为.hpp
#include "person.hpp"
void test01()
{
Person<string, int> p("Tom", 10);
p.showPerson();
}
int main() {
test01();
system("pause");
return 0;
}
总结:主流的解决方式是第二种,将类模板成员函数写到一起,并将后缀名改为.hpp
类模板与友元
学习目标:
掌握类模板配合友元函数的类内和类外实现
- (常用)全局函数(友元函数)类内实现 - 直接在类内(普通类的friend只能外面)声明友元即可
- 全局函数(友元函数)类外实现 - 需要提前让编译器知道全局函数的存在(复杂一点)(注意:普通类的friend只能类外实现)(注意加上<>)
示例:
#include <string>
//全局函数配合友元 类外实现 - 先做函数模板声明,在做友元
template<class T1, class T2> class Person;
//如果声明了函数模板,可以将实现写到后面,
否则需要将实现体写到类的前面让编译器提前看到
//template<class T1, class T2> void printPerson2(Person<T1, T2> & p);
template<class T1, class T2>
void printPerson2(Person<T1, T2> & p) //注意不需要加类的作用域
{
cout << "类外实现 ---- 姓名: " << p.m_Name << " 年龄:" << p.m_Age << endl;
}
template<class T1, class T2>
class Person
{
//1、全局函数配合友元 类内实现(区别于普通的类的friend函数)
friend void printPerson(Person<T1, T2> p)
{
cout << "姓名: " << p.m_Name << " 年龄:" << p.m_Age << endl;
}
//全局函数配合友元 类外实现
friend void printPerson2<>(Person<T1, T2> & p); //这里注意<>
public:
Person(T1 name, T2 age)
{
this->m_Name = name;
this->m_Age = age;
}
private:
T1 m_Name;
T2 m_Age;
};
//1、全局函数在类内实现
void test01()
{
Person <string, int >p("Tom", 20);
printPerson(p);
}
//2、全局函数在类外实现
void test02()
{
Person <string, int >p("Jerry", 30);
printPerson2(p);
}
int main() {
//test01();
test02();
system("pause");
return 0;
}
总结:建议全局函数做类内实现,用法简单,而且编译器可以直接识别
如果类外面:注意需要在类定义前面看到两个东西存在:person类模板,friend的定义(还不大清楚为什么)
类模板案例
案例描述: 实现一个通用的数组类,要求如下:
- 可以对内置数据类型以及自定义数据类型的数据进行存储
- 将数组中的数据存储到堆区
- 构造函数中可以传入数组的容量
- 提供对应的拷贝构造函数以及operator=防止浅拷贝问题
- 提供尾插法和尾删法对数组中的数据进行增加和删除
- 可以通过下标的方式访问数组中的元素
- 可以获取数组中当前元素个数和数组的容量
示例:
myArray.hpp中代码
#pragma once
#include <iostream>
using namespace std;
template<class T>
class MyArray
{
public:
//构造函数
MyArray(int capacity)
{
this->m_Capacity = capacity;
this->m_Size = 0;
pAddress = new T[this->m_Capacity];
}
//拷贝构造
MyArray(const MyArray & arr)
{
this->m_Capacity = arr.m_Capacity;
this->m_Size = arr.m_Size;
this->pAddress = new T[this->m_Capacity];
for (int i = 0; i < this->m_Size; i++)
{
//如果T为对象,而且还包含指针,必须需要重载 = 操作符,因为这个等号不是 构造 而是赋值,
// 普通类型可以直接= 但是指针类型需要深拷贝
this->pAddress[i] = arr.pAddress[i];
}
}
//重载= 操作符 防止浅拷贝问题
MyArray& operator=(const MyArray& myarray) {
if (this->pAddress != NULL) {
delete[] this->pAddress;
this->m_Capacity = 0;
this->m_Size = 0;
}
this->m_Capacity = myarray.m_Capacity;
this->m_Size = myarray.m_Size;
this->pAddress = new T[this->m_Capacity];
for (int i = 0; i < this->m_Size; i++) {
this->pAddress[i] = myarray[i];
}
return *this;
}
//重载[] 操作符 arr[0]
T& operator [](int index)
{
return this->pAddress[index]; //不考虑越界,用户自己去处理
}
//尾插法
void Push_back(const T & val)
{
if (this->m_Capacity == this->m_Size)
{
return;
}
this->pAddress[this->m_Size] = val;
this->m_Size++;
}
//尾删法
void Pop_back()
{
if (this->m_Size == 0)
{
return;
}
this->m_Size--;
}
//获取数组容量
int getCapacity()
{
return this->m_Capacity;
}
//获取数组大小
int getSize()
{
return this->m_Size;
}
//析构
~MyArray()
{
if (this->pAddress != NULL)
{
delete[] this->pAddress;
this->pAddress = NULL;
this->m_Capacity = 0;
this->m_Size = 0;
}
}
private:
T * pAddress; //指向一个堆空间,这个空间存储真正的数据
int m_Capacity; //容量
int m_Size; // 大小
};
类模板案例—数组类封装.cpp中
#include "myArray.hpp"
#include <string>
void printIntArray(MyArray<int>& arr) {
for (int i = 0; i < arr.getSize(); i++) {
cout << arr[i] << " ";
}
cout << endl;
}
//测试内置数据类型
void test01()
{
MyArray<int> array1(10);
for (int i = 0; i < 10; i++)
{
array1.Push_back(i);
}
cout << "array1打印输出:" << endl;
printIntArray(array1);
cout << "array1的大小:" << array1.getSize() << endl;
cout << "array1的容量:" << array1.getCapacity() << endl;
cout << "--------------------------" << endl;
MyArray<int> array2(array1);
array2.Pop_back();
cout << "array2打印输出:" << endl;
printIntArray(array2);
cout << "array2的大小:" << array2.getSize() << endl;
cout << "array2的容量:" << array2.getCapacity() << endl;
}
//测试自定义数据类型
class Person {
public:
Person() {}
Person(string name, int age) {
this->m_Name = name;
this->m_Age = age;
}
public:
string m_Name;
int m_Age;
};
void printPersonArray(MyArray<Person>& personArr)
{
for (int i = 0; i < personArr.getSize(); i++) {
cout << "姓名:" << personArr[i].m_Name << " 年龄: " << personArr[i].m_Age << endl;
}
}
void test02()
{
//创建数组
MyArray<Person> pArray(10);
Person p1("孙悟空", 30);
Person p2("韩信", 20);
Person p3("妲己", 18);
Person p4("王昭君", 15);
Person p5("赵云", 24);
//插入数据
pArray.Push_back(p1);
pArray.Push_back(p2);
pArray.Push_back(p3);
pArray.Push_back(p4);
pArray.Push_back(p5);
printPersonArray(pArray);
cout << "pArray的大小:" << pArray.getSize() << endl;
cout << "pArray的容量:" << pArray.getCapacity() << endl;
}
int main() {
//test01();
test02();
system("pause");
return 0;
}
总结:
能够利用所学知识点实现通用的数组
预处理指令
一般的
#include那种
#define 定义常量
参数宏
#defineMIN(a,b)(a<b ? a : b) 理解:快速的函数编辑,这是除了lamda之后第二个可以快速编辑函数的方法。同时这里结合了快速编译if-else
#include <iostream>
using namespace std;
#define MIN(a,b) (a<b ? a : b)
int main ()
{
int i, j;
i = 100;
j = 30;
cout <<"较小的值为:" << MIN(i, j) << endl;
return 0;
}
条件编译
有选择地对部分程序源代码进行编译
条件预处理器的结构与 if 选择结构很像。
#ifdef NULL
#define NULL 0
#endif
您可以只在调试时进行编译,调试开关可以使用一个宏来实现.如果在指令 #ifdef DEBUG 之前已经定义了符号常量 DEBUG,则会对程序中的 cerr 语句进行编译。
#ifdef DEBUG
cerr << "Varibale x = " << x << endl;
#endif
可以使用 #if 0 语句注释掉程序的一部分,如下所示:
#if 0
不进行编译的代码
#endif
#include <iostream>
using namespace std;
#define DEBUG
#define MIN(a,b) (((a)<(b)) ? a : b)
int main ()
{
int i, j;
i = 100;
j = 30;
#ifdef DEBUG
cerr <<"Trace: Inside main function" << endl;
#endif
#if 0
/* 这是注释部分 */
cout << MKSTR(HELLO C++) << endl;
#endif
cout <<"The minimum is " << MIN(i, j) << endl;
#ifdef DEBUG
cerr <<"Trace: Coming out of main function" << endl;
#endif
return 0;
}
# 和 ## 运算符(简化两个函数)
# 运算符会把 replacement-text 令牌转换为用引号引起来的字符串。
## 运算符用于连接两个令牌
#include <iostream>
using namespace std;
#define MKSTR( x ) #x
int main ()
{
cout << MKSTR(HELLO C++) << endl;
return 0;
}
》》》HELLO C++
#include <iostream>
using namespace std;
#define concat(a, b) a ## b
int main()
{
int xy = 100;
cout << concat(x, y);
return 0;
}
让我们来看看它是如何工作的。不难理解,C++ 预处理器把下面这行:
cout << concat(x, y);
转换成了:
cout << xy;
C++ 中的预定义宏
__LINE__ | 这会在程序编译时包含当前行号。 |
__FILE__ | 这会在程序编译时包含当前文件名。 |
__DATE__ | 这会包含一个形式为 month/day/year 的字符串,它表示把源文件转换为目标代码的日期。 |
__TIME__ | 这会包含一个形式为 hour:minute:second 的字符串,它表示程序被编译的时间。 |
#include <iostream>
using namespace std;
int main ()
{
cout << "Value of __LINE__ : " << __LINE__ << endl;
cout << "Value of __FILE__ : " << __FILE__ << endl;
cout << "Value of __DATE__ : " << __DATE__ << endl;
cout << "Value of __TIME__ : " << __TIME__ << endl;
return 0;
}
Value of __LINE__ : 6
Value of __FILE__ : test.cpp
Value of __DATE__ : Feb 28 2011
Value of __TIME__ : 18:52:48
信号处理在标准库里
多线程在标准库
输入和输出
cin
cout
cerr标准错误流:类似cout,不缓冲,立即输出
clog标准日志流:类似cout,缓冲,这意味着每个流插入到 clog 都会先存储在缓冲区,直到缓冲填满或者缓冲区刷新时才会输出。
通过这些小实例,我们无法区分 cout、cerr 和 clog 的差异,但在编写和执行大型程序时,它们之间的差异就变得非常明显。所以良好的编程实践告诉我们,使用 cerr 流来显示错误消息,而其他的日志消息则使用 clog 流来输出。