TGViewer
Channel Public Channel
C/C++ | Вопросы собесов

C/C++ | Вопросы собесов

@easy_c_plus

Сайт: https://easyoffer.ru/
Все каналы: t.me/+xGeAw6ckJ4liYzQy

Контакт для рекламы: @sendme_ads
Subscribers
4.19K
Photos
36
Videos
0
Links
1.6K

Showing posts older than #2376 · Back to latest

Older Posts 20 shown
Post #2374 529
🤔 Что можешь рассказать о little-endian и big-endian?

Little-endian и big-endian — это порядки байтов в мультибайтовых данных, используемые в компьютерной архитектуре для хранения данных. В little-endian младший байт сохраняется по низкому адресу, а старший байт по высокому. В big-endian старший байт сохраняется по низкому адресу, а младший по высокому. Выбор порядка байтов влияет на совместимость и производительность системы в различных сценариях использования.

Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚Базу знаний
  • 👍 2
Post #2373 632
🤔 Что будет если для беззнаковой переменной равной 0 сделать декремент?

Если вы сделаете декремент (--) для беззнаковой переменной, значение которой равно 0, это приведет к переполнению, и переменная примет максимальное значение, которое может быть представлено этим типом.

Рассмотрим, что произойдет с переменной типа unsigned int, если её значение равно 0 и мы применим к ней декремент:
#include <iostream>

int main() {
unsigned int x = 0;
std::cout << "x before decrement: " << x << std::endl;
x--; // Декрементирование беззнаковой переменной 0
std::cout << "x after decrement: " << x << std::endl; // Переполнение
return 0;
}


Для беззнаковой переменной unsigned int: Диапазон значений: от 0 до 4294967295 (для 32-битной реализации).

Когда переменная x равна 0 и мы выполняем x--, происходит следующее:
🟠Значение 0 уменьшается на 1.
🟠Поскольку unsigned int не может представлять отрицательные значения, происходит переполнение.
🟠Переполнение для беззнакового типа приводит к "оборачиванию" значения через максимальное значение типа.
🟠Максимальное значение для unsigned int (при 32-битной реализации) равно 4294967295.

Таким образом, после выполнения x--, значение x становится 4294967295.
Результат выполнения приведенного выше кода будет следующим:
x before decrement: 0
x after decrement: 4294967295


Для беззнаковых типов в C++ (например, unsigned char, unsigned short, unsigned int, unsigned long): Если значение переменной равно 0 и к ней применяется декремент, результатом будет максимальное значение этого типа.

Ставь 👍 и забирай 📚 Базу знаний
Post #2372 604
🤔 Хорошо ли хранить числа с плавающей запятой в качестве ключа контейнера?

Нет, хранить числа с плавающей запятой как ключи не рекомендуется.
1. Числа с плавающей запятой могут иметь проблемы с точностью, что приведёт к некорректному сравнению ключей.
2. Лучше использовать целые числа или строки в качестве ключей, а дробные значения преобразовывать к целым или строковым представлениям.


Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚Базу знаний
Post #2371 636
🤔 Какая сложность поиска в set и unordered_set?

Контейнеры set и unordered_set представляют собой различные структуры данных, каждая из которых имеет свои особенности по скорости выполнения основных операций, включая поиск. Вот как работают эти контейнеры и какова сложность их операций поиска:

🚩set

Реализуется как сбалансированное двоичное дерево поиска, обычно как красно-черное дерево. Он хранит элементы в отсортированном порядке, что позволяет выполнять двоичный поиск. Сложность поиска: Поиск в нем выполняется за логарифмическое время, \(O(\log n)\), где \(n\) — количество элементов в set. Эта эффективность достигается за счёт использования структуры сбалансированного дерева, которое позволяет быстро делить данные на меньшие сегменты.

🚩unordered_set

Реализуется с использованием хеш-таблицы. Это позволяет, при идеальных условиях, выполнять поиск за константное время. Сложность поиска: В среднем, поиск в нем занимает константное время \(O(1)\). Однако в худшем случае, например, при неудачной работе хеш-функции или при большом количестве коллизий, поиск может деградировать до \(O(n)\). В таких ситуациях все ключи могут оказаться в одной "корзине" или "ведре" (bucket), и для нахождения правильного элемента потребуется просмотреть все элементы в этом ведре.

Для set
#include <iostream>
#include <set>

int main() {
std::set<int> mySet = {5, 3, 9, 1};
auto search = mySet.find(3);
if (search != mySet.end()) {
std::cout << "Found " << *search << std::endl;
} else {
std::cout << "Not found" << std::endl;
}
return 0;
}


Для unordered_set
#include <iostream>
#include <unordered_set>

int main() {
std::unordered_set<int> mySet = {5, 3, 9, 1};
auto search = mySet.find(3);
if (search != mySet.end()) {
std::cout << "Found " << *search << std::endl;
} else {
std::cout << "Not found" << std::endl;
}
return 0;
}


Ставь 👍 и забирай 📚 Базу знаний
Post #2369 586
🤔 Что такое рекурсивный mutex?

Рекурсивный mutex — это тип мьютекса, который позволяет одному и тому же потоку захватывать его несколько раз без блокировки. Это полезно, когда одна и та же функция вызывает себя рекурсивно или косвенно через другие функции и при этом требуется захватить один и тот же мьютекс. В отличие от обычного мьютекса, рекурсивный мьютекс учитывает количество захватов и освобождается только после того, как поток выполнит соответствующее количество вызовов unlock(). Это предотвращает взаимоблокировки при рекурсивных вызовах.

Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚Базу знаний
Post #2368 584
🤔 Что такое POD тип ?

POD (Plain Old Data) тип — это простой тип данных в C++, который совместим с C и обладает определенными свойствами, делающими его поведение предсказуемым и простым.

🚩Основные характеристики POD типов

🟠Тривиальность (Trivial)
Тривиальный конструктор (конструктор по умолчанию, копирования и перемещения). Тривиальный деструктор. Компилятор генерирует их автоматически, если они не определены явно.

🟠Стандартная макетируемость (Standard Layout)
Все нестатические члены данных имеют один и тот же доступ (public, private, или protected). Нет виртуальных функций и нет виртуальных базовых классов. Все базовые классы являются POD типами. Стандартные правила выравнивания и макетирования данных в памяти.

struct MyPOD {
int x;
float y;
};


Пример НЕ POD типа
struct NonPOD {
int x;
virtual void func() {} // Наличие виртуальной функции делает тип не POD
};


🚩Применение POD типов

🟠Совместимость с C
Поскольку POD типы совместимы с C, они могут быть безопасно использованы в смешанных C/C++ программах.

🟠Серилизация
Легко сохранять и загружать из файлов или передавать по сети, так как их память может быть прочитана или записана напрямую.

🟠Оптимизация
Компиляторы могут лучше оптимизировать код с POD типами из-за их простоты и предсказуемости.

🚩Проверка, является ли тип POD

В C++11 и новее можно использовать стандартную библиотеку для проверки, является ли тип POD
#include <type_traits>

struct MyPOD {
int x;
float y;
};

struct NonPOD {
int x;
virtual void func() {}
};

int main() {
std::cout << std::is_pod<MyPOD>::value << std::endl; // вывод: 1 (true)
std::cout << std::is_pod<NonPOD>::value << std::endl; // вывод: 0 (false)
return 0;
}


Ставь 👍 и забирай 📚 Базу знаний
  • 👍 4
Post #2367 578
🤔 Как проверить, вылетело ли исключение в каком-то из потоков?

Исключения из потоков можно сохранить в std::exception_ptr через std::current_exception() и передать их в главный поток для обработки.

Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚Базу знаний
Post #2365 624
🤔 В каком порядке конструируется объект и в каком разрушается?

При создании и удалении объекта в C++ выполняется несколько этапов:
Конструирование: от базового класса к производному.
Деструкторы вызываются в обратном порядке: от производного к базовому.

🚩Порядок вызова конструкторов

При создании объекта сначала создаются его базовые классы, затем члены класса, потом сам объект.
#include <iostream>

class Base {
public:
Base() { std::cout << "Конструктор Base\n"; }
~Base() { std::cout << "Деструктор Base\n"; }
};

class Derived : public Base {
public:
Derived() { std::cout << "Конструктор Derived\n"; }
~Derived() { std::cout << "Деструктор Derived\n"; }
};

int main() {
Derived obj;
}


Вывод
Конструктор Base
Конструктор Derived
Деструктор Derived
Деструктор Base


🚩Порядок вызова конструкторов для членов класса

Члены класса инициализируются в порядке их объявления в классе (а не в порядке в списке инициализации).
class A {
public:
A() { std::cout << "A()\n"; }
~A() { std::cout << "~A()\n"; }
};

class B {
public:
B() { std::cout << "B()\n"; }
~B() { std::cout << "~B()\n"; }
};

class C {
A a;
B b;
public:
C() { std::cout << "C()\n"; }
~C() { std::cout << "~C()\n"; }
};

int main() {
C obj;
}


Вывод
A()
B()
C()
~C()
~B()
~A()


Ставь 👍 и забирай 📚 Базу знаний
Post #2364 523
🤔 Как сделать так, чтобы в shared_ptr были утечки?

Утечки возможны при создании циклических ссылок, например, когда два объекта хранят shared_ptr друг на друга.

Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚Базу знаний
  • 👍 1
Post #2363 599
🤔 Почему по идее в конструкторе нужны исключения?

Необходимы для того, чтобы сигнализировать о неудачной инициализации объекта. Если инициализация объекта не может быть завершена успешно, конструктор должен сообщить об этом, чтобы предотвратить дальнейшее использование объекта в некорректном состоянии.

🟠Гарантия корректного состояния объекта
Конструктор должен гарантировать, что объект находится в корректном состоянии после его создания. Если возникает ошибка при инициализации, использование объекта в некорректном состоянии может привести к непредсказуемым результатам и трудноотлаживаемым ошибкам. Если подключение к базе данных не удалось, исключение сигнализирует о неудачной инициализации.
class DatabaseConnection {
public:
DatabaseConnection(const std::string& connectionString) {
if (!connect(connectionString)) {
throw std::runtime_error("Connection failed");
}
}
private:
bool connect(const std::string& connectionString) {
// Логика подключения
return false; // Имитация ошибки подключения
}
};


🟠Безопасность ресурсов
Конструкторы часто выделяют ресурсы, такие как память, файлы или сетевые соединения. Если инициализация не удалась, необходимо убедиться, что все выделенные ресурсы будут корректно освобождены. Исключения помогают автоматизировать этот процесс, используя RAII (Resource Acquisition Is Initialization). Если файл не удалось открыть, исключение будет выброшено, и деструктор корректно освободит ресурсы.
class FileHandler {
public:
FileHandler(const std::string& filename) {
file = fopen(filename.c_str(), "r");
if (!file) {
throw std::runtime_error("Failed to open file");
}
}
~FileHandler() {
if (file) {
fclose(file);
}
}
private:
FILE* file;
};


🟠Целостность программы
Исключения в конструкторах позволяют предотвратить использование некорректно инициализированных объектов. Это важно для поддержания целостности программы и избежания непредсказуемых ошибок. Если загрузка конфигурации не удалась, исключение предотвращает создание некорректного объекта ConfigLoader.
class ConfigLoader {
public:
ConfigLoader(const std::string& configPath) {
if (!loadConfig(configPath)) {
throw std::runtime_error("Failed to load config");
}
}
private:
bool loadConfig(const std::string& configPath) {
// Логика загрузки конфигурации
return false; // Имитация ошибки загрузки
}
};


🚩Обработка исключений в конструкторах

Исключения в конструкторах должны быть обработаны на более высоком уровне программы, чтобы гарантировать корректное завершение программы или повторение попытки инициализации. В этом примере ошибка инициализации обрабатывается в main, что позволяет программе корректно завершить работу или предпринять другие действия.
int main() {
try {
DatabaseConnection db("connection_string");
} catch (const std::exception& e) {
std::cerr << "Initialization failed: " << e.what() << std::endl;
// Дополнительные действия по обработке ошибки
}
return 0;
}


Ставь 👍 и забирай 📚 Базу знаний
  • 👍 3
Post #2361 509
🤔 Чем отличаются STL-контейнеры vector и array?

1. vector:
- Динамический массив, размер которого можно изменять.
- Управляет памятью автоматически.
- Подходит для сценариев, где размер данных неизвестен заранее.
2. array:
- Статический массив, размер которого фиксирован при создании.
- Не выделяет и не освобождает память динамически.
- Быстрее и эффективнее для небольших данных с фиксированным размером.


Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚Базу знаний
Post #2359 537
🤔 Как называется одинаковый результат после применения хэш функции?

Одинаковый результат после применения хэш-функции называется коллизией.

🚩Что такое коллизия?

Коллизия возникает, когда две разные входные величины (ключи) при обработке одной и той же хэш-функцией дают одинаковое хэш-значение. Это проблема, так как хэш-функции обычно используются для быстрого доступа к данным (например, в хэш-таблицах), и одинаковые хэши для разных данных нарушают уникальность ключей.

🚩Почему коллизии возникают?

🟠Ограниченный размер хэш-значений
Хэш-функция генерирует значения фиксированной длины (например, 32 или 64 бита), поэтому существует конечное количество возможных хэшей. Однако количество входных данных, которые можно подать на вход, теоретически бесконечно.
🟠Особенности алгоритма хэширования
Некоторые хэш-функции менее устойчивы к коллизиям из-за их структуры.

🚩Как справляться с коллизиями?

Есть несколько способов обработки коллизий в хэш-таблицах:

🟠Метод цепочек (chaining)
Каждое значение хэша хранит список элементов, которые получили одинаковый хэш. При добавлении нового ключа он просто добавляется в связанный список.
#include <iostream>
#include <list>
#include <vector>
using namespace std;

class HashTable {
int size;
vector<list<int>> table;

public:
HashTable(int s) : size(s), table(s) {}

int hashFunction(int key) {
return key % size;
}

void insert(int key) {
int index = hashFunction(key);
table[index].push_back(key);
}

void display() {
for (int i = 0; i < size; i++) {
cout << i << ": ";
for (int key : table[i]) {
cout << key << " -> ";
}
cout << "NULL" << endl;
}
}
};

int main() {
HashTable ht(5);
ht.insert(10);
ht.insert(15);
ht.insert(20);
ht.insert(7);
ht.insert(2);
ht.display();
return 0;
}


🟠Открытая адресация (open addressing)
При коллизии новое значение записывается в следующую свободную ячейку в таблице. Используются разные стратегии, такие как линейное пробирование, квадратичное пробирование или двойное хэширование.

🟠Использование криптографических хэш-функций
Устойчивые хэш-функции (например, SHA-256) уменьшают вероятность коллизий, но они медленнее и чаще используются в безопасности.

Ставь 👍 и забирай 📚 Базу знаний
Post #2357 542
🤔 В каком порядке элементы списка инициализируются в конструкторе после двоеточия?

Элементы инициализируются в порядке их объявления в классе, а не в том порядке, в котором они указаны в списке инициализации.
Несовпадение может вызвать предупреждение компилятора.


Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚Базу знаний
Post #2356 608
🤔 Для чего может применяться в С++ виртуальное наследование?

Виртуальное наследование применяется для решения проблемы "ромбовидного наследования" (или "diamond problem"), которая возникает в иерархиях классов с множественным наследованием. Это проблема возникает, когда класс наследуется от двух классов, которые в свою очередь наследуются от одного общего базового класса. В результате создаются две копии базового класса в наследнике, что приводит к неоднозначностям и увеличению использования памяти.

#include <iostream>

class A {
public:
void show() {
std::cout << "Class A" << std::endl;
}
};

class B : public A {};
class C : public A {};
class D : public B, public C {};

int main() {
D obj;
// obj.show(); // Ошибка: неоднозначность, show() есть и в B, и в C
return 0;
}


🚩Решение проблемы с использованием виртуального наследования

Виртуальное наследование решает эту проблему, гарантируя, что в иерархии будет только одна копия базового класса. Это достигается с помощью ключевого слова virtual при наследовании.
#include <iostream>

class A {
public:
void show() {
std::cout << "Class A" << std::endl;
}
};

class B : virtual public A {};
class C : virtual public A {};
class D : public B, public C {};

int main() {
D obj;
obj.show(); // Корректно: вызов метода show() из A
return 0;
}


🚩Как работает виртуальное наследование

🟠Одна копия базового класса
При виртуальном наследовании в наследуемом классе создается только одна копия базового класса, независимо от того, сколько раз он виртуально наследуется.

🟠Оптимизация памяти
Виртуальное наследование предотвращает создание множества копий базового класса, что экономит память.

🟠Явное разрешение неоднозначности
Виртуальное наследование устраняет неоднозначности, связанные с вызовом методов и доступом к членам базового класса.

🟠Пример с конструкторами
При виртуальном наследовании конструкторы базовых классов вызываются инициализирующим классом.
#include <iostream>

class A {
public:
A() {
std::cout << "Constructor of A" << std::endl;
}
};

class B : virtual public A {
public:
B() {
std::cout << "Constructor of B" << std::endl;
}
};

class C : virtual public A {
public:
C() {
std::cout << "Constructor of C" << std::endl;
}
};

class D : public B, public C {
public:
D() {
std::cout << "Constructor of D" << std::endl;
}
};

int main() {
D obj; // Вывод: Constructor of A, Constructor of B, Constructor of C, Constructor of D
return 0;
}


Ставь 👍 и забирай 📚 Базу знаний
  • 🔥 1
Post #2354 549
🤔 С точки зрения синтаксиса, почему нужна возможность бросать исключения из конструктора?

1. Конструкторы не возвращают значения, поэтому исключения — единственный способ сообщить о неудачной инициализации.
2. Исключения интегрируются с механизмами управления памятью, автоматически освобождая частично инициализированные ресурсы.
3. Они делают код более выразительным, отделяя логику инициализации от обработки ошибок.


Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚Базу знаний
Post #2353 601
🤔 Можно использовать в контексте нескольких потоков Shared_ptr и потокобезопасный он или нет?

Использование shared_ptr в многопоточном контексте требует определённой осторожности, поскольку он сам по себе имеет ограниченную потокобезопасность. Рассмотрим подробнее, что это значит и как можно безопасно использовать его в многопоточной среде.

🚩Потокобезопасность

🟠Безопасное копирование и удаление
Стандарт C++ гарантирует, что операции копирования и удаления shared_ptr являются потокобезопасными в том смысле, что вы можете безопасно копировать и удалять разные экземпляры shared_ptr из разных потоков, даже если они указывают на один и тот же объект.

🟠Счётчик ссылок
Модификации счётчика ссылок в нем (т.е. увеличение и уменьшение) являются атомарными операциями, что означает, что при изменении счётчика из разных потоков состояние счётчика останется консистентным и корректным.

🟠Ограничения потокобезопасности
Не рекомендуется изменять сам объект, на который указывает shared_ptr, из нескольких потоков без дополнительной синхронизации, так как shared_ptr не предоставляет потокобезопасность для доступа к объекту. Если несколько потоков изменяют один и тот же shared_ptr (не копии), например, присваивают ему новые значения или обнуляют его, это должно синхронизироваться, так как стандартная библиотека не гарантирует безопасность таких операций без внешней синхронизации.

🚩Рекомендации для использования в многопоточной среде

🟠Избегайте разделять один и тот же `shared_ptr` между потоками
Лучше дать каждому потоку собственную копию shared_ptr, что позволяет избежать конфликтов на запись.

🟠Синхронизируйте доступ к общему shared_ptr
Если несколько потоков должны иметь доступ к одному и тому же shared_ptr, используйте мьютексы или другие механизмы синхронизации для защиты его при записи.

🟠Рассмотрите использование std::atomic<std::shared_ptr<T>> для C++20 и выше
Эта оболочка предоставляет потокобезопасные операции на shared_ptr без необходимости явной синхронизации при изменении указателя.

Ставь 👍 и забирай 📚 Базу знаний
Post #2351 571
🤔 Какая польза от безымянного пространства имен?

Безымянное пространство имен используется для ограничения области видимости имен в файле. Оно предотвращает конфликты имен при работе с глобальными переменными или функциями, так как они становятся доступными только в этом файле.


Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚Базу знаний
  • 👍 1
Post #2350 624
🤔 Какая сложность работы с map и unordered_map?

В std::map и std::unordered_map используются разные структуры данных, поэтому их операции (insert, find, erase) имеют разную сложность.

🚩Почему `std::map` медленнее?

std::map – это самобалансирующееся красно-чёрное дерево, где все операции (insert, find, erase) выполняются за O(log n).
std::map<int, std::string> m;
m[10] = "ten"; // O(log n)
m[5] = "five"; // O(log n)
m[20] = "twenty"; // O(log n)
m.find(5); // O(log n)


Дерево std::map выглядит так
        10
/ \
5 20


🚩Почему `std::unordered_map` быстрее?

std::unordered_map – это хеш-таблица, где insert, find, erase работают за O(1) в среднем.
std::unordered_map<int, std::string> um;
um[10] = "ten"; // O(1)
um[5] = "five"; // O(1)
um[20] = "twenty"; // O(1)
um.find(5); // O(1)


Внутри std::unordered_map выглядит так (разбито по бакетам)
Bucket 0:  ---
Bucket 1: ---
Bucket 2: (10, "ten")
Bucket 3: (5, "five")
Bucket 4: (20, "twenty")


🚩Когда `std::unordered_map` становится медленным (`O(n)`)?

В худшем случае все элементы попадают в один бакет (из-за плохой хеш-функции), тогда поиск превращается в O(n).
std::unordered_map<int, std::string> um;
um[1] = "one";
um[2] = "two";
um[3] = "three";


Ставь 👍 и забирай 📚 Базу знаний
Post #2348 527
🤔 Сложность операции удаления элемента в vector

Удаление элемента в середине или начале std::vector имеет сложность O(n), так как все последующие элементы сдвигаются. Удаление элемента с конца (последнего) — O(1).


Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚Базу знаний
  • 👍 1
Post #2347 599
🤔 Что такое процессы, потоки?

Это независимые программы, выполняющиеся в операционной системе, каждая со своим адресным пространством. Потоки – это единицы выполнения внутри процесса, которые делят общее адресное пространство. Потоки легче создавать и переключать, чем процессы, поэтому они используются для параллельной обработки.

🚩Процессы

Это экземпляр выполняемой программы, который включает в себя программу кода, данные и состояния. Каждый процесс имеет свое собственное адресное пространство и системные ресурсы (например, файлы, память и устройства ввода-вывода).

🟠Изоляция
Процессы изолированы друг от друга. Один процесс не может напрямую изменить данные другого процесса.
🟠Ресурсы
Каждый процесс имеет свое собственное адресное пространство, дескрипторы файлов и т. д.
🟠Создание
Создание нового процесса может быть дорогостоящим по времени и ресурсам, так как включает выделение памяти и инициализацию ресурсов.

#include <iostream>
#include <unistd.h>

int main() {
pid_t pid = fork(); // Создание нового процесса

if (pid == 0) {
// Дочерний процесс
std::cout << "Это дочерний процесс" << std::endl;
} else {
// Родительский процесс
std::cout << "Это родительский процесс, PID дочернего процесса: " << pid << std::endl;
}

return 0;
}


🚩Потоки

Это наименьшая единица выполнения в рамках процесса. Потоки одного процесса разделяют одно адресное пространство и ресурсы, такие как память и дескрипторы файлов.

🟠Разделение ресурсов
Потоки разделяют память и ресурсы процесса, что делает межпоточное взаимодействие более эффективным.

🟠Создание
Создание потоков обычно быстрее и менее ресурсозатратно по сравнению с процессами.

🟠Параллелизм
Потоки позволяют выполнять несколько частей программы параллельно, что улучшает производительность на многоядерных системах.

#include <iostream>
#include <thread>

void printMessage(const std::string& message) {
std::cout << message << std::endl;
}

int main() {
std::thread t1(printMessage, "Привет из потока 1");
std::thread t2(printMessage, "Привет из потока 2");

t1.join(); // Ожидание завершения потока
t2.join();

return 0;
}


Ставь 👍 и забирай 📚 Базу знаний
  • 🔥 1
Older posts →
Threads Profile ViewerView any public Threads profile without an account.Open ThreadLook →Writing with AI? Make it sound human.Metric37 rewrites AI drafts so they read naturally. Free AI detector, 1,500 words free.Try Metric37 →