下学期要开数据结构了,用的是C++,赶紧补一波。看到第八个视频才感觉不行了,要动笔记一下笔记,于是有了这个文章。
小甲鱼C++快速入门戳这里
视频中提到的文章在这里:文章合集
完整笔记看这里:所有视频文字笔记
关于为什么要用C++来讲数据结构,龚老师给出了这么一个解释:
使用基于C++的数据结构的主要理由:
1-后面的编程应用主要基于面向对象;
2-C++编程与C的编程原理一样(结构+算法);
3-C+编程比C编程更简单:真正的:(结构+算法),少传递参数。
学生虽只学习过C,可通过一两次课介绍我们使用到的C–>C++之主要区别,如:输入/输出,模板函数,函数操作内置,成员访问控制,函数与操作符重载,自动构造与析构,动态管理管理……
C语言中字符串是存储在字符数组里面的
char a[10]="I love you."
C++中使用的是std::string来存储
string str=“I love you.”
1.指针所保存的是内存中的一个地址。他并不保存指向的数据的值本身。因此务必确保指针对应一个已经存在的变量,或者一块已经分配了的内存。
2.星号有两种用途。
第一种是用于创建指针:
int *myPointer = &myInt;
第二种是对指针进行解引用:
*myPointer = 1234;
3.C++允许指针群p,就是多个指针有同样的值。
int *p1 = &myInt;
int *p2 = &myInt;
4.C++支持无类型指针(void),就是没有被声明为某种特定类型的指针。例如,
void *vPointer;
对一个无类型指针进行解引用前。必须先把它转换为适当的数据类型。
我们可以将数组的基地址用指针变量保存起来。如果我们想要通过指针访问其他数组元素应该怎么办?
答案:ptr++;
以上并不将地址值不是简单+1处理,而是指向下一个元素。它是按照指向的数组的数据类型的递增的。也就是+sizeof(int)。
C++允许在类里声明常量,但不允许对它进行赋值
class Student
{
public:
const unsigned short SCORE = 80;//出错
}
绕开这个限制的方法是创建一个静态常量
class Student
{
public:
static const unsigned short SCORE = 80;//没问题
}
每个类至少有一个构造器。如果你没有在那里定义一个构造器。编译器就会使用如下语法替你定义一个。
ClassName::ClassName(){}
这是一个没有代码内容的空构造器,除此之外,编译器还会替你创建一个副本构造器(CopyConstructor)。
现有有基类Animal(),子类Pig()和Turtle()。,基类在创造Pig类型对象时最先被调用,如果Pig类也有一个构造器,它将排在第二个被调用。因为基类必须在子类之前初始化原则。
如果构造器带着输入参数,事情就会复杂一些:
class Animal
{
public:
Animal(std::string theName);
std::string name;
};
class Pig::public Animal
{
public:
Pig(std::string theName);
};
正确的写法应该是用Pig的构造器继承Animal的构造器:
Animal::Animal(std::string theName)//这里不变
{
name = theName;
}
Pig::Pig(std::string theName):Animal(std::string theName)
{
}
在上面代码中Pig子类构造器里定义的”:Animal(std::string theName)“语法含义为:
当调用Pig()构造器时(以theName作为输入参数),Animal()构造器也将被调用(theName输入参数将传递给它)。例如:当我们调用Pig pig(“麦兜”);将把字符串"麦兜"传递给Pig()和Animal(),赋值动作实际发生在Animal()方法里面。
与构造器的情况相反,基类的析构器将在子类的最后一条语句执行完毕后才被调用。
静态成员是所有对象共享的,所以不能在静态方法里访问非静态的元素。
非静态方法可以访问类的静态成员,也可以访问类的非静态成员。
在使用静态属性的时候。千万不要忘记为他们分配内存。具体的做法hin简单。只要在类声明的外部对静态属性声明,就像声明一个变量那样即可。
静态方法也可以使用一个普通方法的调用语法来调用,但是建议不要这么做。
请使用:
ClassName::methodName();
不要用:
objectName.methodName();
多态性是面向对象程序设计的一个重要特征。简单的说。多态性是指用一个名字定义不同的函数,调用同一个名字的函数,却执行不同的操作。从而实现传说中的一个接口多种方法。
多态是如何实现绑定的?
编译时的多态性:通过重载实现
运行时的多态性:通过虚函数实现
编译时的多态性特点是运行速度快,运行时的特点是高度灵活和抽象。
一般情况下类的析构函数里面都是释放内存资源的语句。而析构函数不会调用的话,就会造成内存泄露。所以析构器都是虚方法,是为了当一个基类的指针删除一个派生类的对象时。派生类的系构函数可以被正确调用(不然派生类的析构函数不会被调用,非常危险)。另外当类里面有虚函数的时候。编译器会给类添加一个虚函数表。以便存放的虚函数指针。为了节省资源。只有当一个类被用来作为基类的时候,我们才把析构函数写成虚函数。
由于我们没有办法在ostream类(这个类是C++写好的,你敢改?)里面专门添加一个新的operator<<()方法。所以我们只能定义一个正常的函数在外部重载这个操作符,这与重载方法的语法大同小异,唯一的区别是不再有一个对象可以用来调用<<重载函数(在27节中四则运算符重载是对于Rational类而言的),而不得不通过第一个输入参数向这个重载方法传递对象。就是在Rational类上定义友元函数。具体可以看27和28的实例代码。
视频里面没有提到动态数组的销毁,创建既然是new肯定要销毁的。
#include
#include
int main()
{
unsigned int count = 0;
std::cout<<"请输入数组元素个数:\n" ;
std::cin>>count;
int *x = new int[count];
for (int i=0;i
基本类型的对象没有析构函数,所以回收基本类型组成的数组空间用 delete 和 delete[] 都是应该可以的;但是对于类对象数组,只能用 delete[]。对于 new 的单个对象,只能用 delete 不能用 delete[] 回收空间。
所以一个简单的使用原则就是:new 和 delete、new[] 和 delete[] 对应使用。
任何一个函数都不应该把他自己的局部变量的指针作为他的返回值,因为局部变量在栈里,函数结束会自动释放。在调用函数中引用该指针会得到一个垃圾值。
如果你想让一个函数在不会留下任何隐患的情况下返回一个指针,那它只能是一个动态分配的内存块的基地址。动态分配的内存块的基地址是放在堆里面的。他只能用delete进行删除。
两个容易混淆的概念:函数指针和指针函数
函数指针:
指向函数首地址的指针变量称为函数指针。
#include
int fun(int x,int y)
{
int z;
z = (x>y)?x:y;
return z;
}
int main()
{
int i,a,b;
int (*p)(int, int);//定义函数指针。后面的括号代表函数,这是一个指向函数的指针变量
printf("请输入第一个数字:\n");
scanf("%d",&a);
p = &fun;//给函数指针赋值,使它指向函数f
printf("请再输入10个数字:\n");
for(i = 0;i < 10; i++)
{
scanf("%d",&b);
a = (*p)(a,b);//通过指针p调用函数f
}
printf("The max number is:%d\n",a);
return 0;
}
指针函数:一个函数可以带回一个整型数据的值,字符类型的值和实型类型的值,还可以带回指针类型的数据,使其指向某个地址单元。
我们可以把一个对象赋值给一个类型与之相同的变量。编译器将生成必要的代码,把“源”对象各属性的值分别赋值给“目标”对象的对应成员。这种复制行为称之为逐位赋值(bitwise copy)。
这种行为在绝大多数场合都没有问题,但如果某些成员变量是指针的话,问题就来了。对象成员进行逐位赋值的结果是你将拥有两个一模一样的实例,而这两个副本里的同名指针会指向相同的地址。
刚删除其中一个对象时,它包含的指针也将被删除,但万一此时另外一个副本(对象)还在引用这个指针就会出问题。
示例代码:
MyClass obj1;
MyClass obj2;
obj2 = obj1;
上面的代码很简单,分别创建出两个MyClass类的实例obj1和obj2,然后把obj1的值赋值给了obj2,这里就可能出问题。
那么,如何截获这个赋值操作并告诉它应该如何处理那些可嫩出问题的指针呢?
答案是:对操作符=进行重载,在其中对指针进行处理:
MyClass &operator = (const MyClass &rhs);
上面的语句告诉我们这个方法所预期的输入参数是一个MyClass类型的,不可改变的引用。
因为这里使用的参数是一个引用,所以编译器在传递输入参数时就不会再为他创建另外一个副本。(否则可能导致无限递归)
又因为这里只需要读取这个输入参数(被引用对象),而不用改变它的值,所以我们用const把那个引用声明为一个常量,确保万无一失。
如果代码是这个样子
MyClass obj1;
MyClass obj2 = obj1;
这段代码与上面那三行的区别非常小。刚才是先创建两个对象,然后再把obj1的值复制给obj2。现在是先创建一个obj1,然后再创建实例obj2,同时用obj1的值对它进行初始化,虽然看起来好像一样,但编译器却生成完全不同的代码:编译器将在MyClass类里寻找一个副本构造器(copy constructor),如果找不到,他会自行创建一个。
即使我们对复制操作符进行了重载,由编译器创建的副本构造器仍以“逐位赋值”方式,把obj1赋值给obj2。换句话说,如果遇到上面这样的代码,即使已经在这个类里重载了赋值操作符,暗藏隐患的“逐位赋值”行为还是会发生。
因此要想躲开这个隐患,还需要亲自定义一个副本构造器,而不是让系统帮我们生成。
MyClass(const MyClass &rhs);
这个构造器需要一个固定不变(const)的MyClass类型的引用作为输入参数,就像赋值操作符那样,因为它是一个构造器,所以不需要返回类型。
具体代码示例如下:
#include
#include
class MyClass
{
public:
MyClass(int *p);
MyClass(const MyClass &rhs);
~MyClass();
MyClass &operator=(const MyClass &rhs);
void print();
private:
int *ptr;
};
MyClass::MyClass(int *p)
{
std::cout<<"进入主构造器\n";
ptr = p;
std::cout<<"离开主构造器\n";
}
MyClass::MyClass(const MyClass &rhs)
{
std::cout<<"进入副本构造器\n";
*this = rhs;
std::cout<<"离开副本构造器\n";
}
MyClass::~MyClass()
{
std::cout<<"进入主析造器\n";
delete ptr;
std::cout<<"离开主析造器\n";
}
MyClass &MyClass::operator=(const MyClass &rhs)
{
std::cout<<"进入赋值语句重载\n";
if(this != &rhs)
{
delete ptr;
ptr = new int;
*ptr = *rhs.ptr;
}
else
{
std::cout<<"赋值号两边为同一个对象,不做处理!\n";
}
std::cout<<"离开赋值语句重载\n";
return *this;
}
void MyClass::print()
{
std::cout<<*ptr<
输出为:
进入主构造器
离开主构造器
进入主构造器
离开主构造器
进入赋值语句重载
离开赋值语句重载
1
1
-------------------------------
进入主构造器
离开主构造器
进入副本构造器
进入赋值语句重载
离开赋值语句重载
离开副本构造器
3
3
-------------------------------
进入主构造器
离开主构造器
进入赋值语句重载
赋值号两边为同一个对象,不做处理!
离开赋值语句重载
5
进入主析造器
离开主析造器
进入主析造器
离开主析造器
进入主析造器
离开主析造器
进入主析造器
离开主析造器
进入主析造器
离开主析造器
在多个cpp文件中会包含同一个h头文件,意味着头文件中的某个类会被声明多次,这显然没有必要,如果它是一个结构,声明多次还导致编译器报错。
解决方案之一就是把头文件只包含一次,其他的都删除,这样将来会麻烦(因为万一某个cpp文件废弃了就会报错)
另外一个解决方案就是C预处理器,利用预处理器可以让头文件只在这个类还没有被声明过的情况下才声明它,预处理器的条件指令如下图:
#ifdef DEFINE_CLASS
#define DEFINE_CLASS
#enif
以上代码的含义是:如果DEFINE_CLASS还没有定义就定义之。
然后把类声明的代码放到中间,变成:
#ifdef DEFINE_CLASS
#define DEFINE_CLASS
class MyClass
{......};
#enif
在实际项目中DEFINE_CLASS这个名字通常与相应的文件名保持一致,把句点替换为下划线。例如:rational.h写为:RATIONAL_H
预处理器还可以用来注释代码,避免注释的嵌套:
#if 0
//这里有代码
//这里有代码
//这里有代码
#endif
我们已经介绍了两种西加加程序设计范型:
按照面向过程示范型把程序划分成不同的函数。
按照面向对象是范型把代码和数据组织成各种各样的类,并建立类之间的继承关系。
这节将学习新的范型:泛型编程,泛型编程技术支持程序员创建函数和类的蓝图(即模板,template),而不是具体的函数和类。这些模板可以没有任何类型,它们可以处理的数据并不仅限于某种特定的数据类型。
当出需要用到这些函数中的某一个时,编译器将根据模板即时生成一个能够对特定数据类型进行处理的代码版本。
泛型编程技术可以让程序员用一个解决方案解决多个问题。
在泛型编程技术里,我们仍然需要编写自己的函数和类,但不必限定他们所使用的数据类型,只需要使用一个占位符,通常用字母T来表示,然后用这个占位符来编写函数,当程序需要这段代码时,你提供的数据类型,编译器将根据你的模板即时生成实用的代码。
简单的说,编译器把模板里面的每一个T替换为所提供的数据类型。
以下是定义名为foo()的函数模板:
template
void foo(T param)
{
//do something
}
这里有几件事值得注意,第一行代码里,在尖括号里有一个class T,用来告诉编译器字母T将在接下来的函数里代表一种不确定的数据类型。
关键字class并不意味着这个是类,这只是一种约定俗成的写法。在告诉计算机T是一种类型之后,就可以像对待一种普通数据类型那样使用它了。
#include
#include
template
void swap(T &a, T &b)
{
T tmp = a;
a = b;
b = tmp;
}
int main()
{
int i1 = 100;
int i2 = 200;
std::cout<<"交换前,i1="<
在创建模板的时候,还可以用
template //typename可以自己取名字
代替
template
注意:不要把函数模板分成原型和实现两个部分。如果编译器看不到模板的完整代码,它就无法正确地生成代码。
为了明确表明swap()是一个函数模板,还可以使用
swap (i1,i2)
来调用这个函数,这将明确地告诉编译器它应该使用哪一种类型。
另外一个注意:
如果某个函数对所有数据类型都将进行同样的处理,就应该把它写成一个模板。
如果某个函数对不同的数据类型进行不同的处理,就应该对它进行重载。
类模板和函数模板非常相似:同样是先编写一个类的模板,再由编译器再第一次使用这个模板时生成实际代码:
template
class MyClass
{
MyClass();
void swap(T &a,T &b);
};
构造器的实现将是下面这样:
MyClass::MyClass()
{
//初始化操作。
}
因为MyClass是一个类模板,所以不能只写出MyClass::MyClass(),编译器需要你在这里给出一种与MyClass()配合使用的数据类型,必须在尖括号里提供他。因为没有确定的数据类型可以提供,所以使用一个T作为占位符即可。
注意:
C++没有限制只能使用一个类型的占位符,如果类模板需要一种以上的类型,根据具体情况多使用几个占位符即可。
template
class MyClass
{
MyClass();
void fun(T &a,U &b);
};
在实例化的时候,需要这样做:
MyClass myClass;
这里不能直接用new创建
顺便贴一个知识点:
https://www.cnblogs.com/fanhaha/p/7055345.html
C++编程技巧:对象与实例的区别,new与不用new的区别:
class A a; a 在栈里;
class A a=new A; A 在堆里;
new创建类对象需要指针接收,一处初始化,多处使用; CTest* p= new CTest();//new申请的对象,则只有调用到delete时再会执行析构函数 ; 不用new CTest mTest;//不需要手动释放,该类析构函数会自动执行
new需要delete销毁
new创建对象在堆空间
频繁调用不适合用new,要申请和释放内存
用new 生成对象,上面的例子写为:
int main()
{
student* s=new student('a','b');
s->show();
delete(s);
return 0;
}
原文地址
(1)就起作用的阶段而言: #define是在编译的预处理阶段起作用,而const是在 编译、运行的时候起作用。
(2)就起作用的方式而言: #define只是简单的字符串替换,没有类型检查。而const有对应的数据类型,是要进行判断的,可以避免一些低级的错误。
(3)就存储方式而言:#define只是进行展开,有多少地方使用,就替换多少次,它定义的宏常量在内存中有若干个备份;const定义的只读变量在程序运行过程中只有一份备份。
(4)从代码调试的方便程度而言: const常量可以进行调试的,define是不能进行调试的,因为在预编译阶段就已经替换掉了。
(1)const常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误。
(2)有些集成化的调试工具可以对const常量进行调试,但是不能对宏常量进行调试。
(3)const可节省空间,避免不必要的内存分配,提高效率