C/C++杂记

1.拷贝构造函数为什么需要传引用?
传值本身就用到了拷贝构造函数,如果用传值代替传引用,直接陷入递归死循环。

2.string类原理
basic_string是一个模版类,string是模版形参为char的basci_string模版类的类型定义,即typedef

typedef basic_string<char, char_traits<char>, allocator<char>> string;

而npos,是初始化为-1表示没有找到:

static const size_type npos = -1;

basic_string是类模版,并且是容器类模版,basic_string类模版的对象管理的序列是标准的C++ 字符串,basic_string包括string、wstring、u16string和u32string,分别对应char、wchar_t(宽字符)、char16_t(16位char)、char32_t(32位)。

3.浅拷贝和深拷贝
在有指针的情况下,浅拷贝只是增加了一个指针指向已经存在的内存,而深拷贝就是增加一个指针并且申请一个新的内存,使这个增加的指针指向这个新的内存,采用深拷贝的情况下,新对象完全独立于被拷贝的对象。

4.右值引用、lvalue、xvalue、prvalue
https://blog.csdn.net/qq_40586164/article/details/107690601

5.编译相关
https://blog.csdn.net/qq_40586164/article/details/107621976

6.new是否带括号的区别:

  • 对于有默认构造函数的类对象来说没有区别。
  • 对于内置类型:
int *p1=new int;//默认初始化,*p1值未定义
int *p2=new int();//值初始化,*p2值为0

初始化类型:

  1. 列表初始化

    形式:

    [new] T [object] { arg1, arg2, ... };
    

    本质上就是:

    • 如果T是aggregate类型,list中的参数对object成员逐个初始化;
    • 如果T不是aggregate类型,编译器查找最匹配list参数的T的构造函数。

    而在第一种aggregate initialization情况中,如果list参数个数小于T的成员个数,剩余成员进行值初始化。

    在C++11以前,程序员,或者初学者经常会感到疑惑关于怎样去初始化一个变量或者是一个对象。这么多的对象初始化方式,不仅增加了学习成本,也使得代码风格有较大出入,影响了代码的可读性和统一性。

    从C++11开始,对列表初始化(List Initialization)的功能进行了扩充,可以作用于任何类型对象的初始化,至此,列表初始化方式完成了天下大一统。比如下面的四种初始化方式。
    此外,若存在类型转换且具有丢失信息的风险时,编译器将会报错。

    long double ld = 3.1415926536int a{ld}, b = {ld}    // 编译器报错,存在丢失信息的风险
    int c(ld), d = ld ;    //正确
    

  1. 默认初始化(default initialization):

    [new] T object;
    

    如果定义变量时没有指定初值,则变量被默认初始化,此时变量被赋予了“默认值”。默认值到底是什么由变量类型决定,同时定义变量的位置也会对此有影响。

    如果是内置类型的变量未被显式初始化,它的值由定义的位置决定。定义于任何函数体之外的变量被初始化为0,定义在函数体内部的内置类型变量将不被初始化,一个未被初始化的内置类型变量的值是未定义的,如果试图拷贝或以其他形式访问此类值将引发错误。

    每个类各自决定其初始化对象的方式。而且,是否允许不经初始化就定义对象也由类自己决定。如果类允许这种行为(存在默认构造函数),它将决定对象的初始值到底是什么。

    其实这也就是兼容最早的C行为。

    参考:《C++primer v5》P40

  2. 值初始化:
    那么如果在list initialization形式中,没有任何args,也就是

    [new] T [object] {};
    

    值初始化一般是三种处理方式:

    • 如果T有用户定义的缺省构造函数,直接调用;
    • 如果T有编译器生成的缺省构造函数,先零初始化再调用;
    • 如果T根本不是类,直接零初始化。

  3. 零初始化:设置对象的初始值为零。

参考:C++值初始化,默认初始化,以及其他初始化类型的区别? - 很好的回答 - 知乎


C++new的底层实现有两步:
6. 调用一个合适的operator new函数分配足够的内存,默认使用malloc分配,分配失败会抛出异常
(可以强制不抛出异常:new (std::nothrow) xxx() );
7. 调用合适的构造函数初始化这块内存。

当我们重载operator new的时候,重载的是第一步里operator new的内存分配方式,而不是整个new表达式。

7.绝不重新定义继承而来的缺省参数
虚函数中如果存在默认参数值,在派生类中覆盖这个虚函数时改变默认参数值会失效。就算改变了默认参数值,使用的还是基类虚函数的默认参数值。
因为 虚函数是动态绑定的,但是为了执行效率,缺省参数是静态绑定的。使用的是指针类型对应虚函数的参数。
https://blog.csdn.net/sinat_35261315/article/details/53767750?tdsourcetag=s_pcqq_aiomsg

8.定义和声明
定义是一种特殊的声明,它创建了一个对象;
声明简单地说明了在其他地方创建的对象的名字,它允许你使用这个名字。
声明相当于普通的声明:它所说明的并非自身,而是描述其他地方的创建的对象。
定义相当于特殊的声明:它为对象分配内存。

声明的作用是引入名字和型别,而不给出细节,如存储位置或具体实现:

extern int x; // 对象声明
class Widgets; // class声明
bool func(const Widget& w); // 函数声明
enum class Color; // 被作用域包裹的enum声明

定义则会给出存储位置和具体实现的细节:

int x; // 对象定义
class Widget {// class定义
};
bool func(const Widget& w)
{ return w.size() < 10; } // 函数定义
enum class Color
{ Yellow, Red, Blue }; // 被作用域包裹的enum定义

定义同时也可以当声明用。

9.数组名和指针的区别
编译器用数组名标记数组的属性,比如具有确定数量的元素。

只有当数组名在表达式中使用时,编译器才会为它产生一个指针常量,指向其首元素。而只有以下两种情况,才不被当做指针常量:

  • sizeof(数组名):返回数组长度(所占的字节数,不是数组元素个数),而不是指向数组的指针的长度。
  • &数组名:产生一个指向数组的指针,而不是一个指向某个指针常量的指针。

两种不同字符串申明方式:

char	good_template[] = "/tmp/dirXXXXXX";	
char	*bad_template = "/tmp/dirXXXXXX";	

数组是在栈上分配的,字符串可以更改;
申明字符串指针时,只有指针自身驻留在栈上,字符串被存放在可执行文件的只读段,无法修改。

10.const

A. const char *pContent;
B. char  *const pContent;
C. char const *pContent;//同A
D. const char* const pContent; 

沿着 * 号划一条线,
如果const位于 * 的左侧,则const就是用来修饰指针所指向的变量,即指针指向为常量;(底层const,与指针和引用等复合类型的基本类型有关)
如果const位于 * 的右侧,const就是修饰指针本身,即指针本身是常量。 (顶层const,表示任意的对象是常量)
https://www.cnblogs.com/vcfly/archive/2008/12/27/1363562.html
任何对象都可以是顶层const,但如果一个常量对象被其他对象(指针、引用等复合类型)所指向,那这个对象就成为了底层const。

const char *pc ;//指针指向的对象为const,底层const
char* p=const_cast<char*>(pc);//const_cast只能去掉运算对象的底层const

char &r=x;
//任何具有明确定义的类型转换,只要被转换的类型不包括底层const,都可以使用static_cast
const char &rc=static_cast<const char&>(r);

//const_cast和static_cast正好互补

11.运算符重载有类内重载和类外重载(声明为类的友元)两种,但类内重载需要强制类对象位于运算符左边,所以可能出现 Point<<cout ; 因此一般把输出运算符重载放在类外。

12.类型限定符:
volatile关键字:
volatile意为易变的、不稳定的,和const一样,是类型限定符。
用途是告诉编译器不要对此变量上的操作做任何优化
用它声明的类型变量表示可能被某些编译器未知的因素更改,比如:操作系统,硬件或者其他线程等。
因此编译器不会对该变量的代码进行优化,总是从其所在的内存直接读取数据,以防止变量值被外部事件修改后,未能及时更新。

mutable关键字:
容许在即便包含它的对象被声明为 const 时,仍可修改声明为 mutable 的类成员。

13.迭代器
在这里插入图片描述在这里插入图片描述

14.尽量在可行用emplace代替push
对于已有的对象,使用emplace、push、insert没有区别,都是拷贝构造。

但对于需要临时构造的对象,push、insert是构造出临时对象再拷贝到需要的位置。

而将构造参数传入emplace,则emplace用布置 new (placement new)于容器所提供的位置原位构造元素,节约了一次构造。

void emplace_back( Args&&... args );

但也有不能用emplace的情况,emplace无法使用{ }传入临时对象:

struct S {
    int value;
};
a.emplace_back({0});//编译不通过

15.类中定义的指针未初始化时为0,必须new

16.constexpr

对于修饰Object来说,
const并未区分出编译期常量和运行期常量
constexpr限定在了编译期常量
但是,constexpr修饰的函数,返回值不一定是编译期常量。#It is not a bug, it is a feature.#

constexpr修饰的函数,简单的来说,如果其传入的参数可以在编译时期计算出来,那么这个函数就会产生编译时期的值。但是,传入的参数如果不能在编译时期计算出来,那么constexpr修饰的函数就和普通函数一样了。不过,我们不必因此而写两个版本,所以如果函数体适用于constexpr函数的条件,可以尽量加上constexpr。而检测constexpr函数是否产生编译时期值的方法很简单,就是利用std::array需要编译期常值才能编译通过的小技巧。这样的话,即可检测你所写的函数是否真的产生编译期常值了。
https://www.zhihu.com/question/35614219/answer/63798713

17.为什么析构函数大多为虚函数?
将可能会被继承的父类的析构函数设置为虚函数,可以保证当我们new一个子类,然后使用基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄漏。
当基类析构为虚函数时,系统为派生类合成的默认析构函数也是虚函数。

18.类的static静态成员变量:
通常情况下,类的静态成员变量需要在类外定义及初始化(类似全局变量定义在任何函数之外),因为静态成员变量不是由类的构造函数初始化。

但是,可以为const整数类型类型的静态成员提供类内初始化,我找到的解释是:因为static const 成员变量会被编译器优化,为编译期常量,编译器不会为其分配内存,如果想取地址还是得在内外定义。

19.毫秒计时:

long long getTimestampInMilliseconds() {
    using namespace std::chrono;
    return duration_cast<milliseconds>(system_clock::now().time_since_epoch())
        .count();
}

int main()
{
    auto t1 = getTimestampInMilliseconds();
    //...代码
    std::cout<< getTimestampInMilliseconds() - t1 ;
}

sleep指定ms:

//5000ms
std::chrono::milliseconds dura(5000);
std::this_thread::sleep_for(dura);

//或者
std::this_thread::sleep_for(std::chrono::seconds(5));

20.std::ref、std::cref的作用。
thread、bind等函数式编程在传递函数时,即使形参为传引用时,考虑到传入对象的生成周期,会自动将传引用改为传值。因此传引用失效,但使用std::ref修饰的对象会被显式传引用。std::cref即为const引用。

21.unordered_map、map之类的const对象无法取[]符号,因为[]在键值不存在时会自动创建,因此只能使用find。

22.隐式类类型转换。
如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制,有时我们把这种构造函数称作转换构造函数。
能通过一个实参调用的构造函数定义了一条从构造函数的参数类型向类类型隐式转换的规则。

构造函数隐式转换可以通过explicit关键字来抑制。

23.赋值运算符重载函数无论是否返回引用都正确。

Singleton operator=(const Singleton& a);
Singleton& operator=(const Singleton& a);

但是,默认的合成赋值运算符函数是返回引用的。

(a = b) = c;

若返回引用,上式 a = c, 否则a = b,c被赋值给了临时变量(临时变量并非常量)。

24.函数作为实参时,可以传入成员函数,不过需要bind对象。

#include <bits\stdc++.h>

using namespace std;

using f = function<void()>;

void fun(f x){
    x();
}

class Base{
public:
    void function(){ cout<<"member function"<<endl;}
};
void freeFun(){
    cout<<"free function"<<endl;
}
int main() {
    Base b;
    fun(bind(&Base::function, b));
    fun(&freeFun);
    return 0;
}
//member function
//free function

此外函数名fun、&fun以及*fun的含义都是一样的,都是一个函数指针。
因此,上面的&可以省略。

25.引用也可以实现多态:

class base{
public:
    virtual void f(){
        cout<<"1"<<endl;
    }
};

class derive: public base{
public:
    void f(){
        cout<<"2"<<endl;
    }
};

int main() {
    derive d;
    base &b = d;
    b.f();	//输出:2
    return 0;
}

26.更安全的static_cast:
static_cast可以强制执行downcast,不进行检查,容易出问题。
我们可以实现implicit_cast来禁止downcast,实际上是通过implicit conversion。

template<typename To, typename From>
inline To implicit_cast(From const &f)
{
  return f;
}

27.委托构造:
C++11后,类如果有多个构造函数,且有重复部分,则可以使用委托构造。

一个委托构造函数使用它所属类的其他构造函数执行它自己的初始化过程,或者说它把它自己的一些(或者全部)职责委托给了其他构造函数。

class Sales_data {
   public:
    //非委托构造函数使用对应的实参初始化成员
    Sales_data(std::string s, unsigned cnt, double price)
        : bookNo(s), units_sold(cnt), revenue(cnt * price) {}
    //其余构造函数全都委托给另一个构造函数
    Sales_data() : Sales_data("", 0, 0) {}
    Sales_data(std::string s) : Sales_data(s, 0, 0) {}
    Sales_data(std::istream& is) : Sales_data() { read(is, *this); }
    //其他成员与之前的版本一致
};

注意:委托构造必须放在初始化列表中!

28.string_view
C++17引入的string_view为只读字符串类型,类型体积很小。主要包含字符串指针和字符串大小参数。
const char*可以构造string_view,且string可以隐式转换成string_view。
不过要注意:string_view包含的内容生命期一定要比string_view本身长。所以将临时变量赋值给string_view变量是错的!

29.lambda仅在无捕获时可以转换为函数指针

5.1.2 (6) The closure type for a lambda-expression with no lambda-capture has a public non-virtual non-explicit const conversion function to pointer to function having the same parameter and return types as the closure type’s function call operator. The value returned by this conversion function shall be the address of a function that, when invoked, has the same effect as invoking the closure type’s function call operator.

30.拷贝构造/拷贝赋值/移动构造/移动赋值

struct foo {
    foo() {}
    foo(const foo&) { std::cout << "拷贝构造\n"; }
    foo& operator=(const foo& x) {
        cout << "拷贝赋值\n";
        return *this;
    }

    foo(foo&&) { std::cout << "移动拷贝\n"; }
    foo& operator=(foo&& x) {
        cout << "移动赋值\n";
        return *this;
    }
    ~foo() {}
};

31.dynamic_cast
用于将基类的指针或引用安全地转换成派生类的指针或引用。转换失败返回NULL。

32.内存泄漏检测:
1、WINDOWS:VS
必须Debug模式

#define _CRTDBG_MAP_ALLOC
#include <crtdbg.h>
#ifdef _DEBUG
#define DEBUG_NEW new(_NORMAL_BLOCK, __FILE__, __LINE__)
#define new DEBUG_NEW
#endif


int main() {
    _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
    //...
}

2、Linux: Valgrind

33.空指针为什么可以调用成员函数?
静态成员函数可以直接调用,
普通成员函数只是将this指针传了进去,空指针的this为0,也会被传进去。

34.C++空类实现的函数:默认构造、默认析构、拷贝构造函数、赋值运算符、取地址运算符、取地址const。

35.为什么构造函数不能为虚函数?
构造函数必须指定类名,虚函数的使用需要调用虚指针,构造前没有虚指针。

36、常成员函数

class Point {
   public:
    int GetX() const { return xVal; }  //声明常成员函数
    int GetY() const { return yVal; }

   private:
    int xVal, yVal;
};

作用:

  • 保证不可修改对象成员;
  • const对象只能调用const成员函数,无法调用非const成员函数。


C专题

1.C程序启动与终止
在这里插入图片描述
C启动例程如果用C来写,类似exit(main(argc,argv)),不过实际常常用汇编来写。

终止处理程序(出口函数)使用函数int atexit(coid (*func)(void));登记。
标准I/O清理程序:使用fclose函数关闭所有打开的流,造成输出缓冲中的所有数据都被冲洗(写到文件上)。

exit(C库函数)执行步骤:

  1. 调用终止处理程序;
  2. 调用标准I/O清理程序:关闭所有打开的标准I/O流,刷新输出缓冲;
  3. 移除进程创建的临时文件;
  4. 调用_exit函数。

_exit(系统调用)执行步骤:

  1. 关闭所有未关闭的文件描述符,不刷新输出缓冲;
  2. 子进程由Init收养;
  3. 传递SIGCHLD信号给父进程,父进程可以由wait函数取得子进程结束状态。

2.C如何实现函数私有公有?
在头文件中包含公有的函数,在源文件中将内部私有函数声明为static,只有统一文件中的其他函数才能调用它们。

3.C的可变参数列表
在这里插入图片描述
实例:


#include <stdio.h>
#include <stdarg.h>
 
double average(int num,...)
{
 
    va_list valist;
    double sum = 0.0;
    int i;
 
    /* 为 num 个参数初始化 valist */
    va_start(valist, num);
 
    /* 访问所有赋给 valist 的参数 */
    for (i = 0; i < num; i++)
    {
       sum += va_arg(valist, int);
    }
    /* 清理为 valist 保留的内存 */
    va_end(valist);
 
    return sum/num;
}
 
int main()
{
   printf("Average of 2, 3, 4, 5 = %f\n", average(4, 2,3,4,5));
   printf("Average of 5, 10, 15 = %f\n", average(3, 5,10,15));
}

结果:

Average of 2, 3, 4, 5 = 3.500000
Average of 5, 10, 15 = 10.000000

来源

3.文件权限S_IRUSR的意义:
S代表state状态,I来自i-node。
参考

4.void *通用指针
void *可以指向任意类型的数据,亦即可用任意数据类型的指针对void *变量赋值,无需强制类型转换为void *类型。
但是,在C++中,void *赋值给其他类型仍需将其强制类型转换为其他类型;C中不需要强制类型转换。
因为C++是强类型语言,变量的使用要严格符合定义,C不是,所以C可以在不同类型间赋值,不会报错。

    long *x;
    void *p=x;
    
    //C++
    x=(long*)malloc(sizeof(x)); 

	//C
	x=malloc(sizeof(x)); 

5.sprintf函数无法检查目的缓冲区是否溢出,更安全的方法是使用snprintf。
同理,需要小心使用的函数还有gets、strcat、strcpy,应该改为调用fgets、strncat、strncpy,不过strncat、strncpy需要额外在字符串末尾添加\0。

6.char[]数组需要在字符串结尾手动加\0,而char*字符串会自动帮你加\0。

char x[20]="111111111121222";
strncpy(x,"4321",4);//输出为432111111121222
strncpy(x,"4321",5);//输出为4321,因为字符串"4321"末尾有隐藏\0

7.结构体内存对齐规则:

  1. 结构体变量的首地址能够被其最宽基本类型成员的大小所整除;
  2. 结构体每个成员相对于结构体首地址的偏移量都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节;
  3. 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节。

注:Union的自身对齐值按为元素最大自身对齐值来定。数组的对齐值为数组中元素的对齐值。

举例:

struct X
{
    char a;
    double b;
    int c;
}S2;

sizeof(x)=24
在这里插入图片描述
8.字符串中由空格隔开的数字可以用sscanf读取出来。
如果scanf中%d是连着写的如“%d%d%d”,在输入数据时,数据之间不可以用逗号分隔,只能用空白字符(空格或tab键或者回车键)分隔。

sscanf(line, "%ld%ld", &arg1, &arg2);

9.如何在其他文件重新define?
先#undef 后再次#define

10.空悬指针(dangling pointer):指向已经销毁的对象或已经回收的地址;
野指针(wild pointer):指的是未经初始化的指针。

11.如下

#ifndef _xxx_H_

#define _xxx_H_

………  

#endif

ifndef只能解决一个头文件在同一个c文件中被重复定义的问题。不能解决一个该头文件内容在多个文件中被重复定义的问题。

所以如果多个文件都需要该变量,则应该把改变了extern放在头文件中,在源文件中进行定义。

不过#define不同于变量,没有重复定义问题,可以放在头文件中。

12.struct结构体初始化方法:

  1. 定义时赋值struct InitMember test = {-10,3.141590,"method one",0.25};

  2. 定义时乱序赋值,仅能用于.c文件(.cpp文件不支持)

    struct InitMember test = {
        .second = 3.141590,
        .third = "method three",
        .first = -10,
        .four = 0.25
    };
    

13.char*const char*的区别:

  • const char*表示指针指向的对象是常量,无法通过该指针修改,而char*可以:
char greeting[] = "Hello";
char* p = greeting;	//必须指向char[],指向字面值字符串也无法修改
p[0] = 'm';
printf("%s\n", p);

char greeting[] = "Hello";
const char* p = greeting;
p[0] = 'm';  // error
printf("%s\n", p);
  • char*可以赋值给const char*,但反过来不可以。
    因此const char*的使用面更广。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值